网络层次划分
为了使不同计算机厂家生产的计算机能够相互通信,以便在更大的范围内建立计算机网络,国际标准化组织(ISO)在1978年提出了”开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)。它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:物理层(Physics Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表示层(Presentation Layer)、应用层(Application Layer)。其中第四层完成数据传送服务,上面三层面向用户。
除了标准的OSI七层模型以外,常见的网络层次划分还有TCP/IP四层协议以及TCP/IP五层协议,它们之间的对应关系如下图所示:
1. OSI七层网络模型
TCP/IP协议毫无疑问是互联网的基础协议,没有它就根本不可能上网,任何和互联网有关的操作都离不开TCP/IP协议。不管是OSI七层模型还是TCP/IP的四层、五层模型,每一层中都要自己的专属协议,完成自己相应的工作以及与上下层级之间进行沟通。
1.1 物理层
激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性。该层为上层协议提供了一个传输数据的可靠的物理媒体。简单的说,物理层确保原始的数据可在各种物理媒体上传输。物理层需要记住的两个重要设备:中继器与集线器。
物理层要解决的主要问题:
(1)物理层要尽可能地屏蔽掉物理设备和传输媒体,通信手段的不同,使数据链路层感觉不到这些差异,只考虑完成本层的协议和服务。
(2)给其服务用户(数据链路层)在一条物理的传输媒体上传送和接收比特流(一般为串行按顺序传输的比特流)的能力,为此,物理层应该解决物理连接的建立、维持和释放问题。
(3)在两个相邻系统之间唯一地标识数据电路。
物理层主要功能:为数据端设备提供传送数据通路、传输数据。
1.为数据端设备提供传送数据的通路,数据通路可以是一个物理媒体,也可以是多个物理媒体连接而成。一次完整的数据传输,包括激活物理连接,传送数据,终止物理连接。所谓激活,就是不管有多少物理媒体参与,都要在通信的两个数据终端设备间连接起来,形成一条通路。
2.传输数据,物理层要形成适合数据传输需要的实体,为数据传送服务。一是要保证数据能在其上正确通过,二是要提供足够的带宽(带宽是指每秒钟内能通过的比特(BIT)数),以减少信道上的拥塞。传输数据的方式能满足点到点,一点到多点,串行或并行,半双工或全双工,同步或异步传输的需要。
3.完成物理层的一些管理工作。
物理层的媒体包括架空明线、平衡电缆、光纤、无线信道等。通信用的互连设备指DTE和DCE间的互连设备。DTE即数据终端设备,又称物理设备,如计算机、终端等都包括在内。而DCE则是数据通信设备或电路连接设备,如调制解调器等。数据传输通常是经过DTE──DCE,再经过DCE──DTE的路径。互连设备指将DTE、DCE连接起来的装置,如各种插头、插座。LAN中的各种粗、细同轴电缆、T型接、插头,接收器,发送器,中继器等都属物理层的媒体和连接器。 [4]
1.2 数据链路层
数据链路层在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。
为达到这一目的,数据链路必须具备一系列相应的功能,主要功能有:
1)如何将数据合成数据块,在数据链路层中称这种数据块为帧,帧是数据链路层的传送单位;
2)如何控制帧在物理信道上的传输,包括如何处理传输差错,如何调节发送速率以使与接收方相匹配;
3)在两个网络实体之间提供数据链路通路的建立,维持和释放的管理。
数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。
1.2.1 数据链路层是怎样保证数据传输的可靠性
链路层提供的可靠服务是为物理层来服务的,因为最终数据还是要经过物理层来传输,而物理层的传输过程中是可能出错的,比如噪声的影响导致比特传输错误,传输到对面的节点还原链路层数据帧,发现出错,这时候链路层的可靠传输就体现出来了,数据帧会通过一些手段对数据进行校验,来发现数据帧是否错误,如果错误,可以检错重发,向前纠错,反馈校验,检错丢弃等手段来保证两个节点之间传输的数据帧向上层提供的数据是无差错的。
数据链路层是为物理层提供可靠服务的,因为物理层可能导致比特传输差错,数据链路层保证向上层提供的数据是无差错的;
数据链路层只能保证0不会传成1,1不会传成0,即01串一原来的形式传递过去。但是如果在高层传输过程中有一个数据报丢失了,传输层又采用不可靠的协议,则出错。
差错控制
检错编码:主要有奇偶校验码和循环冗余码
1、奇偶校验码
奇偶校验码是奇校验码和偶校验码的统称。如果是奇校验码,在传输的信息后中附加一位校验元,使得最后传输的信息中“1”的个数是奇数;如果是偶校验码,附加一位后,使得最终的信息中的“1”的个数是偶数个。
举个例子:
要发送的数据为1100101,采用奇校验码,则收到的数据是 11001011,采用偶校验码,则收到的数据是11001010
2、冗余校验码(CRC)
给定一个m bit的帧或报文,发生器生成一个r bit的序列,称为帧检测序列。此时形成的帧为m+r 比特,发送方和接收方事先商定一个多项式,使这个带校验码的帧刚好能被预先确定的多项式整除,即无御书则认为无差错。
举个例子:
设待传送的数据M=101001(即m=6),双方商定的多项式为1101(即r = 3),则被除数是101001 000,即待传送的数据M后加r个0,除数是多项式1101,进行模2除法,得余数001,则实际发出去的数据为 101001 001,即待发送的数据M后面加上余数。这样发送的数据肯定能被多项式整除,不能整除的说明出错了。
3、海明码
最常见的纠错编码是海明码,它能发现双比特错,但只能纠正单比特错。海明码“纠错”d位,需要码距为2d+1的编码方案,“检错”d 位,需要码距为d+1。
海明码m位信息插入 r 个校验位组成m+r位码字,它们必须满足 m + r + 1 ();海明码将码字内的位从左至右依次编号,即第一位是1,第二位是2,依次类推··· 编号为2的幂的位(1,2,4,8 ····)是校验位,其余依次填入m位数据。想要知道编号为k的数据位对哪些校验位有影响,可将编号k改写为2的幂的和,如 3 = 1 + 2; 5 = 1 + 4;6 = 2 + 4; 7 = 1 + 2 + 4;如编号为7的位由编号1、2和4的校验位检测。
数据位: 1 2 3 4 5 6 7
代码: P1 P2 D1 P3 D2 D3 D4
Px为校验位,Dx为数据码
举例,如数据码为1101, 则此时D1 = 1;D2 = 1;D3 = 0;D4 = 1;根据公式()算得r = 3
对于数据位拆分 1 = 1;
2 = 2;
3 = 1 + 2;
4 = 4;
5 = 1 + 4;
6 = 2 + 4;
7 = 1 + 2 + 4;
P1可检验的数据位为1,3,5,7,分别对应的代码位是 P1,D1,D2,D4;令P1 D1 D2 D4 = 0;得出P1 = 1;
P2可检验的数据位为2,3,6,7,分别对应的代码位是P2,D1,D3,D4;令P2 D1 D3 D4 = 0;得出P2 = 0;
P3可检验的数据为4,5,6,7,分别对应的代码位是P3,D2,D3,D4;令P3 D2 D3 D4 = 0;得P3 = 0;
因此,海明码的结果为1010101;如果接收放到的是1010101则无差错,如果D3在传输过程中出错由0变成1;那么接收方则收到的是1010111;检测时,P1 D1 D2 D4 = 0;第一位纠错码为0;则无错,P2 D1 D3 D4 = 1;第二位纠错码为1,则出错;P3 D2 D3 D4 = 1;第三位也出错,将三个纠错码从高到低依次排列,得110;转成十进制数为6;则说明第6位数据出错了,即为D3。
流量控制
流量控制是对控制链路上的帧的发送速率,使接收方有足够的缓冲空间来接收帧,主要方法有两种,停止-等待协议和滑动窗口协议,滑动窗口协议又分为后退N帧协议和选择重传协议;停止-等待协议是一种特殊的滑动窗口协议,相当于发送窗口和接收窗口大小为1的滑动窗口协议。
1、停止-等待协议
发送方每发送一帧,都要等待接收方的应答信号才能发送下一帧,接收方每接收一帧都需要反馈一个应答信号来表示可接收下一帧,如下图。如果发送方没有收到接收方的应答信号就一直等待。源站的等待时间超时后,会再次发送刚才的帧。
2、后退N帧协议
发送窗口大于1,接收窗口=1,若采用n比特对帧编号,则其发送窗口Wt的大小满足
1 W -1,若发送窗口大于 -1,则会造成接收方无法正确分辨新帧和旧帧。相比停止等待协议,后退N帧协议的信道利用率得到提高。
如下图,发送方发送了0,1,2,3,4,5,6,7,8帧,但只收到0和1号帧的确认,经过超时后,重新发送1号帧之后的所有帧,即2,3 ,4,5,6,7,8帧。
3、选择重传协议
发送窗口大于1,接收窗口大于1,若采用n比特编号,需满足:接收窗口Wr + 发送窗口Wt ;假定采用累计确认大方法,并且接收窗口Wr 不应该超过发送窗口Wt 。选择重传的接收窗口大于1,那么接收的帧可以是无序的,在接收端设置具有相当容量的缓冲区来暂存那些未按序到达的帧,避免重复传送那些本已正确到达接收端的数据帧。
假如发送方发送了0,1,2号数据帧,现已收到1号帧的确认,而0、2号帧依次超时,由于对1号帧的确认不具累计确认的作用,发送方认为接收方未收到0、2号帧,则需要重传的帧是0和2号帧。
网络层
传输层
传输层的报文都封装在IP数据报里,IP数据报又封装在数据链路层的数据帧里
网络协议
网络协议是为计算机网络中进行数据交换而建立的规则、标准或者说是约定
因为不同用户的数据终端可能采取的字符集是不同的,两者需要进行通信,必须在一定的标准上进行。
下图为不同计算机群之间利用TCP/IP进行通信的示意图。
TCP/IP协议
TCP/IP协议是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。
IP层接收由更低层(网络接口层例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层—-TCP或UDP层;相反,IP层也把从TCP或UDP层接收来的数据包传送到更低层。IP数据包是不可靠的,因为IP并没有做任何事情来确认数据包是否按顺序发送的或者有没有被破坏,IP数据包中含有发送它的主机的地址(源地址)和接收它的主机的地址(目的地址)。
TCP是面向连接的通信协议,通过三次握手建立连接,通讯完成时要拆除连接,由于TCP是面向连接的所以只能用于端到端的通讯。TCP提供的是一种可靠的数据流服务,采用”带重传的肯定确认”技术来实现传输的可靠性。TCP还采用一种称为”滑动窗口”的方式进行流量控制,所谓窗口实际表示接收能力,用以限制发送方的发送速度。**
TCP协议
传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
互联网络与单个网络有很大的不同,因为互联网络的不同部分可能有截然不同的拓扑结构、带宽、延迟、数据包大小和其他参数。TCP的设计目标是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性。
不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
ACK的全称为Acknowledge character,即确认字符,表示接收到的字符无错误。其格式取决于采取的网络协议。TCP报文格式中的控制位由6个标志比特构成,其中一个就是ACK,ACK为1表示确认号有效,为0表示报文中不包含确认信息,忽略确认号字段。
每台支持TCP的机器都有一个TCP传输实体。TCP实体可以是一个库过程、一个用户进程,或者内核的一部分。在所有这些情形下,它管理TCP流,以及与IP层之间的接口。TCP传输实体接受本地进程的用户数据流,将它们分割成不超过64KB(实际上去掉IP和TCP头,通常不超过1460数据字节)的分段,每个分段以单独的IP数据报形式发送。当包含TCP数据的数据报到达一台机器时,它们被递交给TCP传输实体,TCP传输实体重构出原始的字节流。为简化起见,我们有时候仅仅用“TCP”来代表TCP传输实体(一段软件)或者TCP协议(一组规则)。根据上下文语义你应该能很消楚地推断出其实际含义。例如,在“用户将数据交给TCP”这句话中,很显然这里指的是TCP传输实体。
IP层并不保证数据报一定被正确地递交到接收方,也不指示数据报的发送速度有多快。正是TCP负责既要足够快地发送数据报,以便使用网络容量,但又不能引起网络拥塞:而且,TCP超时后,要重传没有递交的数据报。即使被正确递交的数据报,也可能存在错序的问题,这也是TCP的责任,它必须把接收到的数据报重新装配成正确的顺序。简而言之,TCP必须提供可靠性的良好性能,这正是大多数用户所期望的而IP又没有提供的功能。
首部格式
TCP的首部格式如图所示:
—-Source Port是源端口,16位。
—-Destination Port是目的端口,16位。
—-Sequence Number是发送数据包中的第一个字节的序列号,32位。
—-Acknowledgment Number是确认序列号,32位。
—-Data Offset是数据偏移,4位,该字段的值是TCP首部(包括选项)长度除以4。
—-标志位: 6位,URG表示Urgent Pointer字段有意义:
ACK表示Acknowledgment Number字段有意义
PSH表示Push功能,RST表示复位TCP连接
SYN表示SYN报文(在建立TCP连接的时候使用)
FIN表示没有数据需要发送了(在关闭TCP连接的时候使用)
Window表示接收缓冲区的空闲空间,16位,用来告诉TCP连接对端自己能够接收的最大数据长度。
—-Checksum是校验和,16位。
—-Urgent Pointers是紧急指针,16位,只有URG标志位被设置时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。
主要功能
当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。
TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
- 在数据正确性与合法性上,TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和;同时可以使用md5认证对数据进行加密。
- 在保证可靠性上,采用超时重传和捎带确认机制。
- 在流量控制上,采用滑动窗口协议,协议中规定,对于窗口内未经确认的分组需要重传。
在拥塞控制上,采用广受好评的TCP拥塞控制算法(也称AIMD算法)。该算法主要包括四个主要部分:
(1)慢启动
每当建立一个TCP连接时或一个TCP连接发生超时重传后,该连接便进入慢启动阶段。进入慢启动后,TCP实体将拥塞窗口的大小初始化为一个报文段,即:cwnd=1。此后,每收到一个报文段的确认(ACK),cwnd值加1,即拥塞窗口按指数增加。当cwnd值超过慢启动阐值(ssthresh)或发生报文段丢失重传时,慢启动阶段结束。前者进入拥塞避免阶段,后者重新进入慢启动阶段。
(2)拥塞避免
在慢启阶段,当cwnd值超过慢启动阐值(ssthresh)后,慢启动过程结束,TCP连接进入拥塞避免阶段。在拥塞避免阶段,每一次发送的cwnd个报文段被完全确认后,才将cwnd值加1。在此阶段,cwnd值线性增加。
(3)快速重传
快速重传是对超时重传的改进。当源端收到对同一个报文的三个重复确认时,就确定一个报文段已经丢失,因此立刻重传丢失的报文段,而不必等到重传定时器(RTO)超时。以此减少不必要的等待时间。
(4)快速恢复
快速恢复是对丢失恢复机制的改进。在快速重传之后,不经过慢启动过程而直接进入拥塞避免阶段。每当快速重传后,置ssthresh=cwnd/2、ewnd=ssthresh+3。此后,每收到一个重复确认,将cwnd值加1,直至收到对丢失报文段和其后若干报文段的累积确认后,置cwnd=ssthresh,进入拥塞避免阶段。
AIMD英文全称:Additive Increase Multiplicative Decrease。TCP/IP模型中,属于运输层,为了解决拥塞控制的一个方法,即:加性增,乘性减,或者叫做“和式增加,积式减少”。
当TCP发送方感受到端到端路径无拥塞时就线性的增加其发送速度,当察觉到路径拥塞时就乘性减小其发送速度。
TCP拥塞控制协议的线性增长阶段被称为避免拥塞。
当TCP发送端收到ACK,并且没有检测到丢包事件时,拥塞窗口加1;当TCP发送端检测到丢包事件后,拥塞窗口除以2。
While(Sending_Not_Finish)
{
if(Not_Loss_Packet)
{
CongWin++;
}
else
CongWin=[CongWin/2]; //[]的意思是取整
}
TCP如何保证可靠性
TCP是一种可靠传输协议,到底如何保证可靠性呢?TCP协议里面有如下几种机制去保证
一、字节编号机制
编号机制很好理解,就是给TCP的数据段里面的数据部分 ,每个字节都进行编号。
为什么需要编号?
好说,就是为了更清楚的接收和发送。TCP数据是按序的,接收完之后按序组装好,才会交付给上层。
日常生活中也经常遇到这样的情况,你去银行还不得在门口取个号,先取号的先办理,既保证处理事情不乱,也不用大家站着长长的队,叫到号就是你。
二、数据段的确认机制
也就是我们常常听到的确认应答机制,一问一答,保证问的问题,对方一定接收到,如果确实没有接收到就会重复去问。
TCP确认应答就是每一个数据段发送都会收到接收端返回的一个确认号,收到的确认号表示该号前面的数据全部接收。
确认应答机制里面有几个重要的问题,也是面试高频问题,龙叔必须唠叨几句。
1.TCP可以一次连续发送多个数据段
TCP可以连续发送多个数据段,具体发送数据段的多少取决于对方返回的窗口大小。只要满足窗口大小可容纳,Negale 算法处于关闭状态就可以连续发送多个数据段
TCP_NODELAY 选项
设置该选项: public void setTcpNoDelay(boolean on) throws SocketException读取该选项: public boolean getTcpNoDelay() throws SocketException默认情况下, 发送数据采用Negale 算法.
Negale 算法是指发送方发送的数据不会立即发出,而是先放在缓冲区, 等缓存区满了再发出. 发送完一批数据后, 会等待接收方对这批数据的回应,然后再发送下一批数据. Negale 算法适用于发送方需要发送大批量数据, 并且接收方会及时作出回应的场合, 这种算法通过减少传输数据的次数来提高通信效率.如果发送方持续地发送小批量的数据, 并且接收方不一定会立即发送响应数据, 那么Negale算法会使发送方运行很慢. 对于GUI 程序, 如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动), 这个问题尤其突出. 客户端鼠标位置改动的信息需要实时发送到服务器上, 由于Negale 算法采用缓冲, 大大减低了实时响应速度, 导致客户程序运行很慢.
2.仅对连续接受的数据段进行确认
假设你发送了数据段序号为101、201、301、401、501、601,接收端接收到了101、201、501,此时接收端只会返回201的确认,不会返回501确认,因为301和401还没接收到。当收到301和401之后才会返回501的确认(在不超时的情况下)。
3.不连续序号的数据先缓存下来
如上面的例子,接收端收到101、201、501,此时501不能被确认,因为有不连续的数据,但是501的会被缓存在本地,后面收到301、401立即返回501的确认。
三、TCP的超时重传机制
前面两条都是预防和减少出错,超时重传机制是保证TCP在传输过程中数据丢失了一个回复措施。因此超时重传机制是保证可靠性很重要的机制。
每发送一个TCP数据段都会启动一个超时重传计时器(Retransmission Timer,RTT)。如果在计时器时间内没有收到确认应答号,会启动重传,重新发送该数据段。
这里面还有个点,TCP每发送一个数据段不是立刻把该数据段从缓冲区删除的,收到确认应答以后才会从发送队列丢掉。
超时重传原理看起来比较简单,重传的步骤也比较简单,其实也就是如此简单。有一个难的点是,超时重传计时器的时间是一个很复杂的问题。
表面看起来很简单,不就是一次数据发送到出去到接收端收到消息的时间2么?
事情并没有那么简单
一次往返中间经过的网络路段是不固定的,网络拥塞程度*不确定的。
就像你平时开车,导航不可能只给你一条路线,每次给出的路线也会不同,因为道路的拥堵程度不同。
TCP保证可靠性,因此TCP要求不论处在何种网络环境下都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。
TCP目前采用一种自适应的算法计算RTT值。
给定一个初始的RTT值,初始RTT值是6s,后面每次收到确认应答会进行一次计算,计算本次往返的时间和RTT波动,也就是RTT偏差。最终把RTT+RTT偏差得到新的RTT值。
RFC 6298推荐的α值为1/8,即0.125。
数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。
TCP的三次握手与四次挥手
TCP连接建立过程:首先Client端发送连接请求报文,Server端接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。
TCP连接断开过程:假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说”我Client端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,”告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,”告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,”就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。”,Server端收到ACK后,”就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
为什么要三次握手?
在只有两次”握手”的情形下,假设Client想跟Server建立连接,但是却因为中途连接请求的数据报丢失了,故Client端不得不重新发送一遍;这个时候Server端仅收到一个连接请求,因此可以正常的建立连接。但是,有时候Client端重新发送请求不是因为数据报丢失了,而是有可能数据传输过程因为网络并发量很大在某结点被阻塞了,这种情形下Server端将先后收到2次请求,并持续等待两个Client请求向他发送数据…问题就在这里,Cient端实际上只有一次请求,而Server端却有2个响应,极端的情况可能由于Client端多次重新发送请求数据而导致Server端最后建立了N多个响应在等待,因而造成极大的资源浪费!所以,”三次握手”很有必要!
为什么要四次挥手?
试想一下,假如现在你是客户端你想断开跟Server的所有连接该怎么做?第一步,你自己先停止向Server端发送数据,并等待Server的回复。但事情还没有完,虽然你自身不往Server发送数据了,但是因为你们之前已经建立好平等的连接了,所以此时他也有主动权向你发送数据;故Server端还得终止主动向你发送数据,并等待你的确认。其实,说白了就是保证双方的一个合约的完整执行!
使用TCP的协议:FTP(文件传输协议)、Telnet(远程登录协议)、SMTP(简单邮件传输协议)、POP3(和SMTP相对,用于接收邮件)、HTTP协议等。
UDP协议
UDP用户数据报协议,是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。
UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。
UDP与TCP位于同一层,但它不管数据包的顺序、错误或重发。因此,UDP不被应用于那些使用虚电路的面向连接的服务,UDP主要用于那些面向查询—-应答的服务,例如NFS。相对于FTP或Telnet,这些服务需要交换的信息量较小。
每个UDP报文分UDP报头和UDP数据区两部分。报头由四个16位长(2字节)字段组成,分别说明该报文的源端口、目的端口、报文长度以及校验值。UDP报头由4个域组成,其中每个域各占用2个字节,具体如下:
- (1)源端口号;
- (2)目标端口号;
- (3)数据报长度;
- (4)校验值。
使用UDP协议包括:TFTP(简单文件传输协议)、SNMP(简单网络管理协议)、DNS(域名解析协议)、NFS、BOOTP。
TCP与UDP的区别:TCP是面向连接的,可靠的字节流服务;UDP是面向无连接的,不可靠的数据报服务。
DNS协议
DNS是域名系统(DomainNameSystem)的缩写,该系统用于命名组织到域层次结构中的计算机和网络服务,可以简单地理解为将URL转换为IP地址。域名是由圆点分开一串单词或缩写组成的,每一个域名都对应一个唯一的IP地址,在Internet上域名与IP地址之间是一一对应的,DNS就是进行域名解析的服务器。DNS命名用于Internet等TCP/IP网络中,通过用户友好的名称查找计算机和服务。
NAT协议
NAT网络地址转换(Network Address Translation)属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术,它被广泛应用于各种类型Internet接入方式和各种类型的网络中。原因很简单,NAT不仅完美地解决了lP地址不足的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机。
DHCP协议
DHCP动态主机设置协议(Dynamic Host Configuration Protocol)是一个局域网的网络协议,使用UDP协议工作,主要有两个用途:给内部网络或网络服务供应商自动分配IP地址,给用户或者内部网络管理员作为对所有计算机作中央管理的手段。
HTTP协议
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。
HTTP协议包括哪些请求?
GET:请求读取由URL所标志的信息。
POST:给服务器添加信息(如注释)。
PUT:在给定的URL下存储一个文档。
DELETE:删除给定的URL所标志的资源。
HTTP中,POST与GET的区别
- 1)Get是从服务器上获取数据,Post是向服务器传送数据。
- 2)Get是把参数数据队列加到提交表单的Action属性所指向的URL中,值和表单内各个字段一一对应,在URL中可以看到。
- 3)Get传送的数据量小,不能大于2KB;Post传送的数据量较大,一般被默认为不受限制。
- 4)根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的。
- I. 所谓 安全的 意味着该操作用于获取信息而非修改信息。换句话说,GET请求一般不应产生副作用。就是说,它仅仅是获取资源信息,就像数据库查询一样,不会修改,增加数据,不会影响资源的状态。
II. 幂等 的意味着对同一URL的多个请求应该返回同样的结果。
HTTPS协议
HTTPS:简单讲是HTTP的安全版,在HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
HTTPS特点:
内容加密:采用混合加密技术,中间者无法直接查看明文内容
验证身份:通过证书认证客户端访问的是自己的服务器
保护数据完整性:防止传输的内容被中间人冒充或者篡改
Q:HTTP与HTTPS区别?
A: 1.HTTPS需要申请购买CA证书, HTTP不需要
2.HTTP是明文传输,不安全, HTTPS是在HTTP基础上加了SSL层,更安全
3.HTTPS效率低,HTTP效率高
Q:HTTPS传输过程?
A:客户端发起 HTTPS 请求,服务端返回证书,客户端对证书验证,验证通过后本地生成用于改造对称加密算法的随机数,通过证书中的公钥对随机数进行加密传输到服务端,服务端接收后通过私钥解密得到随机数,之后的数据交互通过对称加密算法进行加解密。
Q:为什么需要证书?
A:防止中间人攻击,验证服务器身份
Q:怎么防止的篡改?
A:证书是公开的,虽然中间人可以拿到证书,但私钥无法获取,公钥无法推断出私钥,所以篡改后不能用私钥加密,强行加密客户也无法解密,强行修改内容,会导致证书内容与签名中的指纹不匹配一个举例
在浏览器中输入 http://www.baidu.com/ 后执行的全部过程。
现在假设如果我们在客户端(客户端)浏览器中输入 http://www.baidu.com, 而 baidu.com 为要访问的服务器(服务器),下面详细分析客户端为了访问服务器而执行的一系列关于协议的操作:1)客户端浏览器通过DNS解析到www.baidu.com的IP地址220.181.27.48,通过这个IP地址找到客户端到服务器的路径。客户端浏览器发起一个HTTP会话到220.161.27.48,然后通过TCP进行封装数据包,输入到网络层。
- 2)在客户端的传输层,把HTTP会话请求分成报文段,添加源和目的端口,如服务器使用80端口监听客户端的请求,客户端由系统随机选择一个端口如5000,与服务器进行交换,服务器把相应的请求返回给客户端的5000端口。然后使用IP层的IP地址查找目的端。
- 3)客户端的网络层不用关系应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,不作过多的描述,无非就是通过查找路由表决定通过那个路径到达服务器。
- 4)客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定IP地址的MAC地址,然后发送ARP请求查找目的地址,如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,然后发送IP数据包到达服务器的地址。
1、网络编程
使用套接字来达到进程间的通信1.1、概述
1、计算机网络是通过传输介质、通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来,实现资源共享和数据传输的系统。网络编程就就是编写程序使联网的两个(或多个)设备(例如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持,通过其提供的接口我们可以很方便地进行网络编程。
2、Java是 Internet 上的语言,它从语言级上提供了对网络应用程 序的支持,程序员能够很容易开发常见的网络应用程序。
3、Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制。并 且 Java 实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。
1.2、计算机网络基础
1、概念
把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、 共享硬件、软件、数据信息等资源。
计算机网络是计算机专业必修的一门学科!里面涉及到计算机之间的通信、网络安全等方方面面,有时间可以自行去学习,关于更多的网络基础知识这里就不一一介绍了。
推荐文章:https://www.jianshu.com/p/ae5e1cee5b04
2、网络编程的目的
直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。
3、网络编程中有两个主要的问题
①如何准确地定位网络上一台或多台主机;定位主机上的特定的应用
②找到主机后如何可靠高效地进行数据传输
1.3、网络通信要素概述
1、我们需要知道的是主机间通过网络进行通信是需要遵循网络通信协议,是通过IP地址准确定位主机,通过端口号准确定位主机上的应用。
IP地址和端口号
网络通信协议
2、如何实现网络中的主机互相通信?
① 通信双方地址:IP和端口号
② 一定的规则(即:网络通信协议。有两套参考模型)
OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广。
TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。
3、网络通信协议(以TCP/IP模型为例)
TCP/IP,即Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议、Internet国际互联网络的基础。
1.4、IP地址和端口号(组合就是网络套接字)
我们知道IP地址和端口号是通信要素之一,它们可以唯一确定某一台主机的某个应用,并为主机之间通信提供了可能!那么什么是IP地址和端口号呢?
1、IP 地址:InetAddress(在Java中使用InetAddress类代表IP)
一的标识 Internet 上的计算机(通信实体)
本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
P地址分类方式1:IPV4 和 IPV6
IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已 经用尽。以点分十进制表示,如192.168.0.1
IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
IP地址分类方式2:公网地址(万维网使用)和私有地址(局域网使用)。192.168. 开头的就是私有址址,范围即为192.168.0.0–192.168.255.255,专门为组织机 构内部使用
特点:不易记忆
2、InetAddress类
Internet上的主机有两种方式表示地址:
①域名(hostName):www.baidu.com
②IP 地址(hostAddress):14.215.177.38
InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address
InetAddress 类对象含有一个 Internet 主机地址的域名和IP地址:www.baidu.com 和 14.215.177.38
域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS) 负责将域名转化成IP地址,这样才能和主机建立连接。 ———-域名解析
InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取InetAddress实例
public static InetAddress getLocalHost()
public static InetAddress getByName(String host)
InetAddress提供了如下几个常用的方法:
public String getHostAddress() //返回 IP 地址字符串(以文本表现形式) 。
public String getHostName() //获取此 IP 地址的主机名
public boolean isReachable(int timeout) //测试是否可以达到该地址
例子
/*
1 java中使用InetAddress代表IP
2 IP的分类:IPV4和IPV6 万维网和局域网
3 域名:www.baidu.com www.cnblogs.com
4 本地回路地址:127.0.0.1 对应着:localhost
5 如何实例化InetAddress类的对象,两个静态方法:
InetAddress getByName(String host)
InetAddress getLocalHost()
6 两个常用方法
getHostName()
getHostAddress()
7 端口号:正在计算机上运行的进程
*/
public class InetAddressTest {
public static void main(String[] args){
try {
// File file = new File("test.txt");
InetAddress IP1 = InetAddress.getByName("192.168.3.2");
System.out.println(IP1);
InetAddress IP2 = InetAddress.getByName("www.baidu.com");
System.out.println(IP2);
// 获取本地IP
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
// getHostName()
System.out.println(IP2.getHostName());
// getHostAddress()
System.out.println(IP2.getHostAddress());
} catch(UnknownHostException e){
e.printStackTrace();
}
}
}
3、端口号
端口号就是标识正在计算机上运行的进程(程序)
不同的进程有不同的端口号
被规定为一个 16 位的整数 0~65535。
端口分类:
① 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口 80,FTP占用端口21,Telnet占用端口23)
② 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占 用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
③ 动态/私有端口:49152~65535
4、端口号与IP地址的组合得出一个网络套接字:Socket
1.5、网络协议
1、网络通信协议
计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代 码结构、传输控制步骤、出错控制等制定标准。
2、问题:网络协议太复杂
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩 解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?
3、通信协议分层的思想
在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常 用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与 再下一层不发生关系。各层互不影响,利于系统的开发和扩展。
4、TCP/IP协议簇
传输层协议中有两个非常重要的协议:
- 传输控制协议TCP(Transmission Control Protocol)
- 户数据报协议UDP(User Datagram Protocol)
TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得 名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP层、传输层和应用层。
5、TCP 和 UDP
TCP协议:
✔ 使用TCP协议前,须先建立TCP连接,形成传输数据通道
✔ 传输前,采用“三次握手”方式,点对点通信,是可靠的
✔ TCP协议进行通信的两个应用进程:客户端、服务端。
✔ 在连接中可进行大数据量的传输
✔ 传输完毕,需释放已建立的连接,效率低
UDP协议:
✔ 将数据、源、目的封装成数据包,不需要建立连接
✔ 每个数据报的大小限制在64K内
✔ 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
✔ 可以广播发送
✔ 发送数据结束时无需释放资源,开销小,速度快
1.6、三次握手与四次挥手
1、三次握手
第一步,请求端(客户端)发送一个包含SYN标志的TCP报文,SYN即同步(Synchronize),同步报文会指明客户端使用的端口以及TCP连接的初始序号;
第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgment)。
第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。
2、四次挥手
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。
服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
服务器关闭客户端的连接,发送一个FIN给客户端。
客户端发回ACK报文确认,并将确认序号设置为收到序号加1。
2、TCP网络编程
2.1、Socket介绍
1、利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实 上的标准。
2、网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。
3、通信的两端都要有Socket,是两台机器间通信的端点。
4、网络通信其实就是Socket间的通信。
5、Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
6、一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
7、Socket分类:
- 流套接字(stream socket):使用TCP提供可依赖的字节流服务
- 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
2.2、Socker类的常用方法
public InputStream getInputStream()返回此套接字的输入流。 可以用于接收网络消息
public OutputStream getOutputStream()返回此套接字的输出流。 可以用于发送网络消息
public InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的, 则返回 null。
public InetAddress getLocalAddress()获取套接字绑定的本地地址。 即本端的IP地址
public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字, 则返回 0。
public int getLocalPort()返回此套接字绑定到的本地端口。 如果尚未绑定套接字, 则返回 -1。 即本端的端口号。
public void close()关闭此套接字。 套接字被关闭后, 便不可在以后的网络连接中使用(即无法重新连接或重新绑定) 。 需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和OutputStream。
public void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容, 则流将返回 EOF(文件结束符) 。 即不能在从此套接字的输入流中接收任何数据。
public void shutdownOutput()禁用此套接字的输出流。 对于 TCP 套接字, 任何以前写入的数据都将被发送, 并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。
2.3、基于Socket的TCP编程
1、Java语言的基于套接字编程分为服务端编程和客户端编程,其通信模型如图所示
2、客户端Socket的工作过程包含以下四个基本的步骤:
创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务端的通信路线。若连接失败,则会出现异常。
打开连接到 Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输
按照一定的协议对 Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入路线的信息),通过输出流将信息写入线程
关闭 Socket:断开客户端到服务器的连接,释放线路
3、客户端创建Socket对象:
客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连 接。Socket的构造器是:
// 构造器一
Socket(String host,int port)throws UnknownHostException,IOException
/* 向服务器(域名是 host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。*/
// 构造器二
Socket(InetAddress address,int port)throws IOException
/* 根据InetAddress对象所表示的 IP地址以及端口号port发起连接。*/
客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请,简要步骤如下
Socket s = new Socket("192.168.40.165",9999); // 1、创建Socket对象,指明服务端的IP和端口号
OutputStream out = s.getOutputStream(); // 2、获取一个输出流,用于输出数据
out.write("hello".getBytes()); // 3、写出数据
s.close(); // 4、回收资源
4、服务器(服务端)程序的工作过程包含以下四个基本的步骤:
调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口 上。用于监听客户端的请求。
调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信 套接字对象。
调用 该Socket类对象的 getOutputStream() 和 getInputStream ():获取输出 流和输入流,开始网络数据的发送和接收。
关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字。
5、服务器建立 ServerSocket 对象
ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口 中的业务员。也就是说,服务器必须事先建立一个等待客户请求建立套接字 连接的ServerSocket对象。
所谓“接收”客户的套接字请求,就是accept()方法会返回一个 Socket 对象
ServerSocket ss = new ServerSocket(9999); // 1、创建服务端的ServerSocket,指明自己的端口号
Socket s = ss.accept (); // 2、调用accept()监听来自客户端的连接
InputStream in = s.getInputStream(); // 3、获取输入流,读取输入流的数据
byte[] buf = new byte[1024];
int num = in.read(buf);
String str = new String(buf,0,num);
System.out.println(s.getInetAddress().toString()+":"+str);
s.close(); // 4、回收资源
ss.close();
2.4、TCP编程简单C/S通信示例
/**
* @description: TCP编程,模拟基于C/S架构客户端与服务端间的通信
* @author: laizhenghua
* @date: 2020/11/28 20:08
*/
public class SocketTest {
/* 客户端 */
@Test
public void client(){
OutputStream output = null;
Socket socket = null;
try {
InetAddress localHost = InetAddress.getByName("127.0.0.1");
socket = new Socket(localHost,8848);
output = socket.getOutputStream();
output.write("hello I'm the client".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(output != null){
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/* 服务端 */
@Test
public void server() {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream input = null;
ByteArrayOutputStream out = null;
try {
serverSocket = new ServerSocket(8848);
socket = serverSocket.accept();
System.out.println("client IP: " + socket.getInetAddress());
input = socket.getInputStream();
/*
一般不建议这样书写,数据传输时可能会出现乱码!!
byte[] buffer = new byte[1024];
int len;
while((len = input.read(buffer)) != -1){
String data = new String(buffer,0,len);
System.out.println(data);
}*/
out = new ByteArrayOutputStream();
byte[] buffer = new byte[10];
int len;
while((len = input.read(buffer)) != -1){
out.write(buffer,0,len);
}
System.out.println(out.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(out != null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(input != null){
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.5、TCP编程实现C/S文件传输
实现功能:客户端发送文件给服务端,服务端将文件保存在本地。
/**
* @description: TCP编程,客户端发送文件给服务端,服务端将文件保存在本地。
* @author: laizhenghua
* @date: 2020/11/28 20:08
*/
public class TCPSocketTest {
/* 客户端 */
@Test
public void client() {
Socket socket = null;
OutputStream writer = null;
BufferedInputStream bis = null;
try {
socket = new Socket(InetAddress.getByName("127.0.0.1"),8089);
writer = socket.getOutputStream();
bis = new BufferedInputStream(new FileInputStream(new File("me.jpg")));
byte[] buffer = new byte[1024];
int len;
while((len = bis.read(buffer)) != -1){
writer.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bis != null){
try {
bis.close();
System.out.println("发送成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/* 服务端 */
@Test
public void server() throws IOException { // 这里异常应该使用try-catch-finally
ServerSocket socket = new ServerSocket(8089);
System.out.println("正在等待客户端连接...");
Socket clientSocket = socket.accept();
System.out.println("客户端已连接IP地址为:"+clientSocket.getInetAddress().getHostName());
InputStream is = clientSocket.getInputStream();
BufferedOutputStream reader = new BufferedOutputStream(new FileOutputStream(new File("new_me.jpg")));
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
reader.write(buffer,0,len);
}
System.out.println("接收成功");
socket.close();
clientSocket.close();
is.close();
reader.close();
}
}
2.5、TCP编程实现C/S信息反馈
实现功能:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给 客户端。并关闭相应的连接。
*
/**
* @description: TCP编程,从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给 客户端。并关闭相应的连接。
* @author: laizhenghua
* @date: 2020/11/28 20:08
*/
public class TCPSocketTest2 {
/* 客户端 */
@Test
public void client() { Socket socket = null; OutputStream writer = null; BufferedInputStream bis = null; try { socket = new Socket(InetAddress.getByName(“127.0.0.1”),8089); writer = socket.getOutputStream();
bis = new BufferedInputStream(new FileInputStream(new File("me.jpg")));
byte[] buffer = new byte[1024];
int len;
while((len = bis.read(buffer)) != -1){
writer.write(buffer,0,len);
}
// 关闭数据的输出
socket.shutdownOutput();
// 接收服务端反馈的信息并输出到控制台
InputStream is = socket.getInputStream();
ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
byte[] buf = new byte[10];
int l;
while((l = is.read(buf)) != -1){
byteArray.write(buf,0,l);
}
System.out.println(byteArray.toString());
is.close();
byteArray.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bis != null){
try {
bis.close();
System.out.println("发送成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/* 服务端 */
@Test
public void server() throws IOException { // 这里异常应该使用try-catch-finally ServerSocket socket = new ServerSocket(8089); System.out.println(“正在等待客户端连接…”); Socket clientSocket = socket.accept();
System.out.println("客户端已连接IP地址为:"+clientSocket.getInetAddress().getHostName());
InputStream is = clientSocket.getInputStream();
BufferedOutputStream reader = new BufferedOutputStream(new FileOutputStream(new File("new_me.jpg")));
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
reader.write(buffer,0,len);
}
System.out.println("接收成功");
// 服务端给客户端反馈信息
OutputStream os = clientSocket.getOutputStream();
os.write("你好客户端,照片已经收到".getBytes());
socket.close();
clientSocket.close();
is.close();
reader.close();
os.close();
} }
<a name="cf3e2664"></a>
## 案例
<a name="e475617a"></a>
### 案例1
描述:客户端给服务器发消息,服务器回复消息。
```java
/**
* 用于服务器端
*
*/
@Test
public void service(){
ServerSocket server = null;
Socket socket = null;//该方法是个阻塞方法,如果没有客户端连接,则一直处于等待中
try {
//1.准备一个ServerSocket
server = new ServerSocket(8877);
System.out.println("服务器已启动");
//2. 监听一个客户端的连接
socket = server.accept();
System.out.println("已连接一个客户。");
//3.1 获取输入流,用来接收该客户端发送的数据
InputStream in = socket.getInputStream();
//3.2 获取输出流,用来向该客户端发送数据
OutputStream out = socket.getOutputStream();
//4 通信
//4.1 接收客户端的消息并在控制台输出
byte[] data = new byte[1024];
int len;
while((len = in.read(data)) != -1){
System.out.print(new String(data, 0, len));
}
//4.2 发送数据
out.write("你好,我是服务器端,你的来信我已收到".getBytes());
out.flush();//刷新,写出消息
} catch (IOException e) {
e.printStackTrace();
}finally{
if(socket != null){
try {
//5 关闭socket,不再与该客户端进行通信,socket一旦关闭,意味着InputStream和OutputStream也关闭了
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(server != null){
try {
//6 如果不再接收任何客户端的消息,则可以关闭ServerSocket
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Test
/**
* 用户客户端
*/
public void client() {
Socket socket = null;
try {
//1. 准备Socket,连接服务器,需要制定服务器的ip地址和端口号
socket = new Socket("127.0.0.1", 8877);
//2. 获取输入输出流,用来接收和传输数据
//2.1 获取输入流,用来接收服务器端的数据
InputStream in = socket.getInputStream();
//2.2 获取输出流,用来向服务器端发送数据
OutputStream out = socket.getOutputStream();
//3. 通信
//3.1 发送数据
out.write("hello,你好啊,我是客户端".getBytes());
out.flush();//需要主动发送
socket.shutdownOutput();//会在流的末尾写一个“流的末尾”,服务端才能读到-1, 否则对方的读取方法一直处于阻塞状态
//3.2 接收服务器端的数据
byte[] data = new byte[1024];
int len;
while((len = in.read(data)) != -1){
System.out.print(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}finally{
if(socket != null){
try {
//4. 关闭socket,即断开与服务器的连接,不再与服务器进行通信,同时InputStream和OutputStream也会关闭
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
案例2
描述:从服务器端给客户端发文件,客户端保存草本地并回复收到。
@Test
/**
* 服务器端,发送文件
*/
public void Server(){
ServerSocket server = null;
Socket socket = null;
BufferedInputStream bis = null;
try {
//1 创建ServerSocket对象,指定端口号
server = new ServerSocket(8887);
//2 使用accept()接收客户端的连接
socket = server.accept();
//3 读取本地文件
//3.1 创建字节输入流BufferedInputStream用于读取本地文件信息
bis = new BufferedInputStream(new FileInputStream(new File("beauty.jpg")));
//4 通信
//4.1 使用套接字连接创建输出流
OutputStream out = socket.getOutputStream();
//4.2 使用套接字连接创建输入流
InputStream in = socket.getInputStream();
//5 读取本地文件传输给客户端
byte[] data = new byte[1024];
int len;
while((len = bis.read(data)) != -1){
// String s = new String(data, 0, len);//容易乱码
out.write(data,0, len);
}
out.flush();
socket.shutdownOutput();
//6 接收客户端消息
while((len = in.read(data)) != -1){
System.out.print(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bis != null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Test
/**
* 客户端,接收文件并回复
*/
public void Client(){
Socket socket = null;
BufferedOutputStream bos = null;
try {
//1 创建socket实例,指定ip地址与端口,连接服务器
socket = new Socket("127.0.0.1", 8887);
//2 使用socket创建输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//3 创建本地连接,用于保存接收到的数据
bos = new BufferedOutputStream(new FileOutputStream(new File("beauty1.jpg")));
//4 通信
//4.1 接收服务器端的数据,并保存到本地
byte[] data = new byte[1024];
int len;
while((len = in.read(data)) != -1){
bos.write(data, 0, len);
}
//4.2 向服务器端返回接收成功
out.write("文件已收到".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、UDP网络编程
3.1、UDP网络通信
1、类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
2、UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
3、DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
4、UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。
3.2、UDP网络通信流程
1、DatagramSocket与DatagramPacket
2、建立发送端,接收端
3、建立数据包
4、调用Socket的发送、接收方法
5、关闭Socket
注意:发送端与接收端是两个独立的运行程序
3.3、UDP网络通信代码实现
/*
UDP网络编程:
✔ 将数据、源、目的封装成数据包,不需要建立连接
✔ 每个数据报的大小限制在64K内
✔ 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
✔ 可以广播发送
✔ 发送数据结束时无需释放资源,开销小,速度快
*/
public class UDPSocketTest {
@Test // 发送端
public void send() throws IOException {
//1 准备DatagramSocket发射台
DatagramSocket socket = new DatagramSocket();
//2 准备DatagramPacket数据包
byte[] data = "hello world".getBytes();
DatagramPacket packet = new DatagramPacket(data,0,data.length, InetAddress.getLocalHost(),8080);
//3 使用DatagramSocket发射数据包DatagramPacket
socket.send(packet);
socket.close();
}
@Test // 接收端
public void receiver() throws IOException {
//1 准备DatagramSocket接受台,需要指定端口号
DatagramSocket socket = new DatagramSocket(8080);
//2 准备DatagramPacket接收数据
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
//3 通信,接收数据
socket.receive(packet);
System.out.println(new String(packet.getData(),0,packet.getLength()));
socket.close();
}
}
DatagramSocket
public void close()关闭此数据报套接字。
public void send(DatagramPacket p)从此套接字发送数据报包。 DatagramPacket 包含的信息指示:将要发送的数据、 其长度、 远程主机的 IP 地址和远程主机的端口号。
public void receive(DatagramPacket p)从此套接字接收数据报包。 当此方法返回时, DatagramPacket的缓冲区填充了接收的数据。 数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。 数据报包对象的 length 字段包含所接收信息的长度。 如果信息比包的长度长, 该信息将被截短
public InetAddress getLocalAddress()获取套接字绑定的本地地址。
public int getLocalPort()返回此套接字绑定的本地主机上的端口号。
public InetAddress getInetAddress()返回此套接字连接的地址。 如果套接字未连接, 则返回 null。
public int getPort()返回此套接字的端口。 如果套接字未连接, 则返回 -1。
DatagramPacket类的常用构造方法
public DatagramPacket(byte[] buf,int length)构造 DatagramPacket, 用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。
public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包, 用来将长度为 length 的包发送到指定主机上的指定端口号。 length参数必须小于等于 buf.length。
DatagramPacket类的常用方法
public InetAddress getAddress()返回某台机器的 IP 地址, 此数据报将要发往该机器或者是从该机器接收到的
public int getPort()返回某台远程主机的端口号, 此数据报将要发往该主机或
者是从该主机接收到的。
public byte[] getData()返回数据缓冲区。 接收到的或将要发送的数据从缓冲区
中的偏移量 offset 处开始, 持续 length 长度。
public int getLength()返回将要发送或接收到的数据的长度。
4、URL网络编程
4.1、URL介绍
1、URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一 资源的地址。
2、它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate 这个资源。
3、通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
4、URL的基本结构由5部分组成: <传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
例如: http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
片段名:即锚点,例如看小说,直接定位到章节
参数列表格式:参数名=参数值&参数名=参数值…
5、Restful风格
一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。restful风格在实际开发中使用较多,对于URL地址有全新的使用方式,可以自行了解restful风格的使用!
推荐文章:https://blog.csdn.net/m0_46357847/article/details/109753731
4.2、URL类与类的构造器
1、为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初 始化一个 URL 对象:
public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:URL url = new URL (“https://www.baidu.com/“);
public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。 例如:URL downloadUrl = new URL(url, “download.html”);
public URL(String protocol, String host, String file); 例如:new URL(“http”, “www.atguigu.com”, “download. html”);
public URL(String protocol, String host, int port, String file); 例如: URL gamelan = new URL(“http”, “www.atguigu.com”, 80, “download.html”);
2、URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通 常是用 try-catch 语句进行捕。
4.3、URL类常用方法
一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:
public String getProtocol( ) 获取该URL的协议名
public String getHost( ) 获取该URL的主机名
public String getPort( ) 获取该URL的端口号
public String getPath( ) 获取该URL的文件路径
public String getFile( ) 获取该URL的文件名
public String getQuery( ) 获取该URL的查询名
4.4、针对http协议的URLConnection类
URL的方法openStream():能从网络上读取数据
若希望输出数据,例如想服务端的CGI(公共网关接口-Common GatewayInterface的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才鞥呢对其进行读写,此时需要使用URLConnection
URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个URL对象上通过方法openConnection()生成对应的URLConnection对象。如果连接过程失败,将产生IOException
URL netchinaren = new URL (“http://www.atguigu.com/index.shtml”);
URLConnection u = netchinaren.openConnection( );
通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互
public Object getContent( ) throws IOException
public int getContentLength( )
public String getContentType( )
public long getDate( )
public long getLastModified( )
public InputStream getInputStream( )throws IOException
public OutputSteram getOutputStream( )throws IOException
URI、URL和URN的区别
URI,是uniform resource identifier的简写,即统一资源标识符,用来唯一的标识一个资源
URL,是uniform resource locator的简写,即统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源
URN,是以一种抽象的,高层次概念定义统一资源标识。
URL和URN则是具体的资源标识方法。URL和URN都是一种URI
Netty
Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序
Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的
Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
参考:netty学习笔记 - wq9 - 博客园 (cnblogs.com)
应用场景
互联网行业
分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。比如阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,而Dubbo默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信
游戏行业
Netty作为高性能的基础通信组件,提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈、账号登录服务器等。而且地图服务器之间也可以方便的通过Netty进行高性能的通信
大数据领域
经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行垮界点通信。它的Netty Service是基于Netty框架的二次封装实现的
I/O模型
简单理解就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
java共支持三种网编编程模型:BIO、NIO、AIO
Java BIO编程
BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求服务器端时就要启动一个线程进行处理,如果这个连接不做任何事情,会造成不必要的线程开销。是Blocking(阻塞) I/O的简称
工作流程
服务器端启动一个ServerSocket
客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯
客户端发出请求后,先资讯服务器是否有线程响应,如果没有则会等待,或者被拒绝
如果有响应,客户端线程会等待请求结束后,再继续执行
应用实例
实例说明:
- 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯
- 要求使用线程池机制改善,可以连接多个客户端
- 服务器端可以接受客户端发送的数据(telnet方式即可)
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制//思路
//1.创建一个线程池
//2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建 ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个 handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息:"+Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过 socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if(read != -1) {
System.out.println(new String(bytes, 0, read)); //输出客户端发送的数据
} else {
break;
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("关闭和 client的连接");
try {
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
问题
每个请求都需要创建独立的线程,与对应的客户端进行数据Read->业务处理->数据Write。当并大数较大时,需要创建大量线程来处理连接,系统资源占用较大。连接建立以后,如果当前线程暂时没有数据刻度,则线程就会被阻塞在Read操作上,造成线程资源的浪费
Java NIO编程
NIO:non-blocking IO,即同步非阻塞,服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。当有事件触发是,服务器端得到通知,进项相应的处理。
NIO相关类都被放在java.nio包及其子包下,并且对java.io包中的很多类进行改写。
特点
NIO有三大核心部分:Channel(管道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区,或者说是面向快编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞市的高伸缩性网络
NIO的非阻塞模式,使一个线程从某管道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,也不会保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某管道,但不需要等待它完全写入,这个线程同事可以去做别的事情。
NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理,不像之前的阻塞IO那样,非得分配10000个线程
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个骑牛,而且并发请求的数量比HTTP1.1大了好几个数量级
NIO三大核心
Channel
NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写操作,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读取数据,也可以写数据到缓冲中
Channel在NIO中是一个接口:public interface Channel extends Closeable{}
常用的Channel类有:FileChannel(用于文件的数据读写)、DatagramChannel(用于UDP的数据读写)、ServerSocketChannel(类似ServerSocket,用于TCP的数据读写)和SocketChannel(类似Socket,用于TCP的数据读写)
Buffer
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制。能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络等途径读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
Selector
当用一个线程去处理多个客户端的连接时,需要用到选择器。Selector能够检测多个注册在自身的管道上是否有事件发生,如果有事件发生,变获取时间然后针对每个时间进行相应的处理
只有在连接或者通道中真正有读写事件发生时,才会进行读写,这样就大大的减少了系统开下,并且不必为每个连接都创建一个线程,也不必要去维护多个线程,这也避免了多线程之间的上下文切换导致的开销
应用案例
群聊系统
要求:
编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
实现多人群聊
服务器端可以检测用户上线、离线,并实现消息转发功能
客户端通过Channel可以无阻塞发送消息给其他所有用户,同时可以接收其他用户发送的消息(由服务器转发得到)
package com.awei.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* 服务端
*/
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer() {
try {
//得到选择器
selector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该 listenChannel注册到 selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (IOException e) {
e.printStackTrace();
}
}
//监听
public void listen() {
try {
//循环处理
while (true) {
int count = selector.select();
if(count > 0) {//有事件处理
//遍历得到 selectionKey集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//取出 selectionkey
SelectionKey key = iterator.next();
//监听到 accept
if(key.isAcceptable()) {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该 sc注册到 seletor
sc.register(selector, SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress() + "上线 ");
}
if(key.isReadable()) { //通道发送 read事件,即通道是可读的状态
//处理读 (专门写方法..)
readData(key);
}
//当前的 key删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
}catch (Exception e) {
e.printStackTrace();
}finally{
}//发生异常处理....
}
//读取客户端消息
private void readData(SelectionKey key) {
//取到关联的 channle
SocketChannel channel = null;
try {
//得到 channel
channel = (SocketChannel) key.channel();
//创建 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据 count的值做处理
if(count > 0) {
//把缓存区的数据转成字符串
String msg = new String(buffer.array());
//输出该消息
System.out.println("form客户端: " + msg);
//向其它的客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
}catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + "离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
}catch (IOException e2) {
e2.printStackTrace();;
}
}
}
//转发消息给其它客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self ) throws IOException{
System.out.println("服务器转发消息中...");
//遍历所有注册到 selector上的 SocketChannel,并排除 self
for(SelectionKey key: selector.keys()) {
//通过 key取出对应的 SocketChannel
Channel targetChannel = key.channel();
//排除自己
if(targetChannel instanceof SocketChannel && targetChannel != self) {
//转型
SocketChannel dest = (SocketChannel)targetChannel;
//将 msg存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将 buffer的数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
package com.awei.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1"; //服务器的 ip
private final int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器,完成初始化工作
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将 channel注册到 selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
//向服务器发送消息
public void sendInfo(String info) {
info = username + "说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if(readChannels > 0) {//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isReadable()) {
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
iterator.remove(); //删除当前的 selectionKey,防止重复操作
} else {
//System.out.println("没有可以用的通道...");
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每个 3秒,读取从服务器发送数据
new Thread() {
@Override
public void run() {
while (true) {
chatClient.readInfo();
try {
sleep(3000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
零拷贝(减少CPU拷贝)
java中常用的零拷贝有mmap(内存映射)和sendFile
package com.awei.nio.zeroCopy;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
//服务器
public class NewIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
//创建 buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readcount = 0;
while (-1 != readcount) {
try {
readcount = socketChannel.read(byteBuffer);
}catch (Exception ex) {
// ex.printStackTrace();
break;
}
byteBuffer.rewind(); //倒带 position = 0 mark作废
}
}
}
}
package com.awei.nio.zeroCopy;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件 channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发
long startTime = System.currentTimeMillis();
//在 linux下一个 transferTo方法就可以完成传输
//在 windows下一次调用 transferTo只能发送 8m ,就需要分段传输文件,而且要主要
//传输时的位置 =》课后思考...
//transferTo底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 =" + transferCount + "耗时 :" + (System.currentTimeMillis() -startTime));
//关闭
fileChannel.close();
}
}
NIO存在的问题
类库和API繁杂
开发工作量和难度都非常大:客户端有时会面临断连重连】网络闪断、半包读写、失败缓存、网络堵塞和异常流的处理等等
Epoll Bug
解决,使用netty,netty对nio的api进行了封装
NIO和BIO的比较
BIO以流的方式处理数据,而NIO以块的方式处理数据。而块I/O的效率比流I/O的效率高很多
BIO是阻塞的,NIO则是非阻塞的
BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到管道中,而Selector可以用于监听多个通道的事件,比如连接请求、数据到达等,因此可以使用单个线程监听多个客户端通道
Java AIO编程
AIO:Asynchronous I/O。即异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
BIO、NIO、AIO的适用场景
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中
NIO方式适用于连接数目较多且连接比较短的架构,比如聊天服务器、弹幕系统、服务器间通信等
AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作
Netty编程
优点
解决了NIO所带来的问题
高性能、吞吐量高、延迟低、减少资源消耗、最小化不必要的内存复制
安全。拥有完整的SSL/TLS和StartTLS支持
Netty线程模式
主要基于主从Reactor多线程模型做了一定的改进。目前存在的线程模型有传统阻塞I/O服务模型(采用阻塞IO模式获取输入的数据,每个连接都需要独立的线程完成数据的输入和业务的处理,这样当并发数很大时,就会创建大量的线程,占用很大的系统资源。当连接创建后,如果当前线程暂时没有数据刻度,该线程会阻塞在read操作,造成线程资源浪费)和Reactor模式,而Reactor模式根据Reactor的数量和处理资源线程池的数量不同又分为单Reactor单线程、但Reactor多线程和主从Reactor多线程
BOSSGroup线程维护Selector,只关注Accept,专门负责接收客户端的连接
当接收到Accept事件,获取到对应的SocketChannel,封装成NIOSocketChannel,并注册到Worker线程(事件循环),并进行维护
当WorkerGroup线程监听到selector中管道发生自己感兴趣的事件后,就进行处理,由已加入通道的handler处理。WorkerGroup专门负责网络的读写
BOSSGroup和WorkerGroup类型都是NIOEventLoopGroup,NIOEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件是一个NIOEventLoop。NIOEventLoop表示一个不断循环的执行处理任务的线程,每个NIOEventLoop都有一个selector(选择器)和一个taskQueue(任务队列),用于监听绑定在其上的socket的网络通讯,可以注册监听多个NIOChannel,每个NIOCHannel只会绑定在唯一的NIOEventLoop上,且都绑定有自己的ChannelPipeline
NIOEventLoopGroup可以有多个线程,即可以含有多个NIOEventLoop
BossGroup中每个NIOEventLoop循环执行的步骤:
- 轮询accept事件
- 处理accept事件,与Client建立连接,生成NIOSocketChannel,并将其注册到某个worker NIOEventLoop上的selector
- 处理任务队列的任务,即runAllTasks
WorkerGroup中每个NIOEventLoop循环执行的步骤
- 轮询read,write事件
- 处理I/O事件,即read,write事件,在对应NIOSocketChannel处理
- 处理任何队列的任务,即runAllTasks
每个Worker NIOEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了Channel,即通过pipeline可以获取到对应通道,管道中维护了很多的处理器
入门实例-TCP服务
要求:
Netty服务器在6668端口监听,客户端能发送消息给服务器
服务器可以回复消息给客户端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建 BossGroup和 WorkerGroup
//说明
//1.创建两个线程组 bossGroup和 workerGroup
//2. bossGroup只是处理连接请求 ,真正的和客户端业务处理,会交给 workerGroup完成
//3.两个都是无限循环
//4. bossGroup和 workerGroup含有的子线程(NioEventLoop)的个数
//默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用 NioSocketChannel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) //设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象(匿名对象)
//给 pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
}); //给我们的 workerGroup的 EventLoop对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步,生成了一个 ChannelFuture对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync()
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.awei.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道 pipeline ,通道 channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 " + Thread.currentThread().getName());
System.out.println("server ctx =" + ctx);
System.out.println("看看 channel和 pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接,出站入站
//将 msg转成一个 ByteBuf
//ByteBuf是 Netty提供的,不是 NIO的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端~(>^ω^<)喵", CharsetUtil.UTF_8));
}
//处理异常,一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
package com.awei.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) //设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture要分析,涉及到 netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
package com.awei.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
异步模型
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者
Netty中的I/O操作时异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture
在Netty中,调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
Netty的异步模型是建立在Future和callBack(回调)之上的。
Future
表示异步的执行结果,可以通过它提供的方法来来检测执行是否完成,比如检索计算等等
ChannelFuture是一个接口,可以实现它并添加监听器,当监听的时间发生时,就会通知监听器
在使用Netty进行编程式,拦截操作和转换出入站数据只需要提供callback或利用Future即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码
Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来
Future-Listener机制
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作
常用的操作:
通过isDone方法来判断当前操作是否完成;
通过isSuccess方法来判断已完成的当前操作是否成功;
通过getCause方法来获取已完成的当前操作失败的原因;
通过isCancelled方法来判断已完成的当前操作是否被取消;
通过addListener方法来注册监听器,当操作已完成(即isDone方法返回完成),将会通知指定的监听器
//绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑
//绑定一个端口并且同步,生成了一个 ChannelFuture对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给 cf注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668成功");
} else {
System.out.println("监听端口 6668失败");
}
}
};
入门实例-http服务
Netty可以做http服务开发,并且能理解handler实例和客户端及其请求的关系
Netty服务器在6668端口监听浏览器发出的请求,并能够给哭护短发送消息,且能够对特定的请求资源进行过滤
package com.awei.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,
workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(6668).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.awei.netty.http;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一个 netty提供的 httpServerCodec codec =>[coder - decoder]
//HttpServerCodec说明
//1. HttpServerCodec是 netty提供的处理 http的编-解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//2.增加一个自定义的 handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
}
}
package com.awei.netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import java.net.URI;
/*
说明
1. SimpleChannelInboundHandler是 ChannelInboundHandlerAdapter
2. HttpObject客户端和服务器端相互通讯的数据被封装成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//channelRead0读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断 msg是不是 httprequest请求
if(msg instanceof HttpRequest) {
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" +
this.hashCode());
System.out.println("msg类型=" + msg.getClass());
System.out.println("客户端地址" + ctx.channel().remoteAddress());
//获取到
HttpRequest httpRequest = (HttpRequest) msg;
//获取 uri,过滤指定的资源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico,不做响应");
return;
}
//回复信息给浏览器 [http协议]
ByteBuf content = Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_8);
//构造一个 http的相应,即 httpresponse
FullHttpResponse
response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//将构建好 response返回
ctx.writeAndFlush(response);
}
}
}
核心模块组件
Bootstrap和ServerBootstrap
Bootstrap意思是引导一个Netty应用,主要作用是配置整个Netty程序,串联各个组件。Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
Future和ChannelFuture
Netty中所有的IO操作都是异步的,不能立刻得知消息是否正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,它们可以注册一个监听,当操作执行完成或失败时会自动触发注册的监听事件
Channel channel();//返回当前正在进行io操作的通道
ChannelFuture sync();//等待异步操作执行完毕
Channel
用于执行网络I/O操作
通过Channel可获得当前网络连接的通道状态,也可获得网络连接的配置参数(例如接收缓冲区大小)
Channel提供异步的网络I/O操作(如建立连接、读写、绑定端口等),异步调用意味着任何I/O调用都将会立即返回,并且不保证在调用结束时请求的I/O操作,已完成的调用会立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,当I/O操作成功、失败或者取消时回调通知调用方
支持关联I/O操作与对应的处理程序
不同协议、不同阻塞类型的连接都有不同的Channel类型与之对应
Selector
Netty基于Selector对象实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件
当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询这些注册的Channel是否有已就绪的I/O事件(例如可读、可写、网络连接完成等),这样程序就可以很简单的使用一个线程高效的管理多个Channel
ChannelHandler及其实现类
ChannelHandler是一个接口,处理I/O事件或者拦截I/O操作,并将其转发到其它ChannelPipeline(业务处理链)中的下一个处理程序
经常需要自定义一个Handler类去继承ChannelHandlerAdapter,然后通过重写相应方法实现业务逻辑
Pipeline和ChannelPipeline
ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。可以理解为ChannelPipeline是保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站事件
ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互
在Netty中,每个Channel都有且仅有一个ChannelPipeline与之对应
ChannelHandlerContext
保存Channel相关的所有上线文信息,同时关联一个ChannelHandler对象。即ChannelHandlerContext中包含一个具体的事件处理器,同事也绑定了对应的Pipeline和Channel的信息,方便对ChannelHandler进行调用
ChannelOption
Netty在创建Channel实例后,一般都需要设置ChannelOption参数
EventLoopGroup及其实现类
EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例
EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Nett服务器端编程中,一般都需要两个EventLoopGroup:BOSSEventLoopGroup和WorkerEventLoopGroup
通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。
BOOSEventLoopGroup负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理
Unpooled类
Netty提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类
package com.atguigu.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyByteBuf01 {
public static void main(String[] args) {
//创建一个 ByteBuf
//说明
//1.创建对象,该对象包含一个数组 arr ,是一个 byte[10]
//2.在 netty的 buffer中,不需要使用 flip进行反转
//底层维护了 readerindex和 writerIndex
//3.通过 readerindex和 writerIndex和 capacity,将 buffer分成三个区域
// 0---readerindex已经读取的区域
// readerindex---writerIndex ,可读的区域
// writerIndex -- capacity,可写的区域
ByteBuf buffer = Unpooled.buffer(10);
for(int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
System.out.println("capacity=" + buffer.capacity());//10
//输出
for(int i = 0; i<buffer.capacity(); i++) {
System.out.println(buffer.getByte(i));
}
for(int i = 0; i < buffer.capacity(); i++) {
System.out.println(buffer.readByte());
}
System.out.println("执行完毕");
}
}
package com.atguigu.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.Charset;
public class NettyByteBuf02 {
public static void main(String[] args) {
//创建 ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));
//使用相关的方法
if(byteBuf.hasArray()) { // true
byte[] content = byteBuf.array();
//将 content转成字符串
System.out.println(new String(content, Charset.forName("utf-8")));
System.out.println("byteBuf=" + byteBuf);
System.out.println(byteBuf.arrayOffset()); // 0
System.out.println(byteBuf.readerIndex()); // 0
System.out.println(byteBuf.writerIndex()); // 12
System.out.println(byteBuf.capacity()); // 36
//System.out.println(byteBuf.readByte()); //
System.out.println(byteBuf.getByte(0)); // 104
int len = byteBuf.readableBytes(); //可读的字节数 12
System.out.println("len=" + len);
//使用 for取出各个字节
for(int i = 0; i < len; i++) {
System.out.println((char) byteBuf.getByte(i));
}
//按照某个范围读取
System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8")));
System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8")));
}
}
}
编码解码器
Handler链的调用机制
案例
群聊系统
心跳机制
通过WebSocket编程实现服务器和客户端的长连接
TCP粘包和拆包问题及其解决方案
有图服务端一次读取到的字节数是不确定的,固存在:
服务端一次性读取多个数据包,这些数据包会粘合在一起,称之为TCP粘包
当服务端多次读取到数据包,且有读取到完整的,也有读取到不完整的数据包时,称之为TCP拆包