前言:
在互联网技术里,有两件事最为重要,一个是TCP/IP协议,它是万物互联的事实标准;另一个是Linux操作系统,它是推动互联网技术走向繁荣的基石。在网络编程中最重要的模型便是OSI七层网络模型和TCP/IP四层网络模型
一、网络模型
网络模型通常指的是计算机网络中的一种抽象表示,它描述了网络中各个组件之间的关系、通信规则和数据流动方式。网络模型有两个主要方面:体系结构和协议。
网络体系结构(Network Architecture): 这一方面描述了网络的整体结构和组织方式,包括网络的层次结构、拓扑结构、连接方式等。其中,最著名的网络体系结构是OSI(开放系统互联)模型和TCP/IP(传输控制协议/互联网协议)模型。
OSI模型(Open Systems Interconnection): OSI模型是国际标准化组织(ISO)定义的一个网络体系结构,包含七个层次,从物理层到应用层,每一层负责特定的功能。这有助于不同厂商的设备能够更好地协同工作。
TCP/IP模型: TCP/IP是互联网所采用的网络体系结构,它包含四个层次:网络接口层、互联网层、传输层和应用层。TCP/IP模型是实际互联网使用的模型,而且是最为广泛接受的。
网络协议(Network Protocol): 这一方面描述了网络中数据如何在不同层次上进行传输和处理。协议是一组规则,定义了数据通信的格式、顺序和错误检测等。常见的网络协议包括TCP、IP、HTTP、FTP等。
TCP(传输控制协议): 负责在数据传输时确保可靠性和顺序性。
IP(互联网协议): 用于在网络中定位和寻址设备,使数据能够正确路由到目标。
HTTP(超文本传输协议): 用于在网络上传输超文本(如网页)的协议。
FTP(文件传输协议): 用于在网络上传输文件的协议。
1.1 OSI模型的七层模型
为使不同国家不同的通信设备能够互相通信,以便在更大的范围内建立计算机网络,有必要建立一个国际范围的网络体系结构标准。
1.2 TCP/IP四层、五层模型
1.3 IP协议及其报文
IP位于网络层,负责承载传输层各种协议的信息。它能将数据包传递到目的地,但并不保证数据包的可靠、有序和完整交付。IP协议的主要职责是进行数据包的路由和转发。对于数据的可靠性、有序性和完整性的需求,这是由传输层的协议(例如TCP)来处理的。目前,IP协议有两个版本,即IPv4和IPv6。
报文格式
以太网的MTU最大传输单元是1500,IP报文小于这个数字就无需分段了
报文经过多少跳的路由,从哪个IP发往哪个IP,报文有多大,优先级高还是低
释义:
版本:IPv4或IPv6
首部长度:数据包的头部长度有多少
服务类型:服务类型字段用于指定IP数据包的服务质量要求,包括优先级、延迟、吞吐量和可靠性等方面的需求。
段偏移:表示当前分段在原始数据报中的偏移量。它指定了分段的数据相对于原始数据报的起始位置的偏移量。
上层协议:上一层也就是传输层是TCP还是其他的协议
报头校验和:用于检测IP首部在传输过程中是否发生了错误。校验和是一种简单的错误检测机制,通过对首部的各个字段进行数学运算,生成一个校验和值,发送端计算并将其添加到IP首部,而接收端则使用相同的算法重新计算,并将结果与接收到的校验和进行比较。
1.4 TCP协议及报文结构
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的协议,它位于 OSI 模型(七层网络模型)的传输层。TCP主要用于在网络上可靠地传输数据,确保数据的有序性、可靠性和完整性。应用程序在使用TCP之前,必须先建立TCP连接。
TCP协议的主要特点:
-
面向连接: 在进行数据传输之前,TCP需要先建立连接,确保通信的两端都同意进行数据交换。连接的建立和终止使用三次握手和四次挥手过程。
-
可靠性: TCP通过序号、确认号、重传机制和校验和等手段,确保数据的可靠传输。如果一个数据包未被确认,发送端将会重传它。
-
有序性: TCP保证数据的有序交付,接收方能够按照发送方的顺序正确接收并重组数据。
-
流控制: TCP使用滑动窗口机制进行流量控制,确保发送和接收的速率相匹配,防止数据溢出。
-
拥塞控制: TCP通过拥塞窗口和慢启动等算法,自适应地调整发送速率,以防止网络拥塞。
报文结构
源端口号:发送出数据的应用程序的端口号
目的端口号:指定接收信息的程序,(一个程序只能监听一个端口号)
序(列)号(Sequence Number 简称seq):序列号是TCP报文中用于标识每个字节的唯一数字。它表示在一个TCP连接中发送的字节的顺序。TCP协议使用序列号来对传输的数据进行编号,以确保数据的有序性。作用是让接收方能够正确地重组和排序收到的数据
确认序号(Transmission Control Protocol 简称ack):确认号是TCP报文中用于确认已成功接收的字节的序列号。它表示接收方期望从发送方接收的下一个字节的序列号。实现了TCP的可靠数据传输,接收方收到数据后,会向发送方发送带有确认号的ACK报文,告诉发送方“我已成功接收到你发送的这些字节,下一个期望接收的字节是这个序列号”。
首部长度:用来指示TCP头部的长度,以4字节为单位
控制标记:包括URG、ACK、PSH、RST、SYN和FIN等标志,用于控制TCP连接的状态。
接收窗口(也称窗口大小):它是TCP协议中的一个重要参数。接收窗口的概念涉及到滑动窗口协议,用于控制在任何时候可以发送给发送方的未被确认的数据量。窗口大小表示接收方还有多少可用的缓冲区空间,用于接收数据。发送方通常会根据接收窗口的大小来控制发送的数据量,以确保不会超出接收方的缓冲区容量。TCP使用滑动窗口协议来进行流量控制,确保发送方不会发送过多的数据导致接收方无法处理。接收窗口的大小会动态调整,根据网络的情况和接收方的处理能力来进行优化。
校验和:校验和(Checksum)是一种用于检测数据传输过程中是否发生错误的简单校验方法。在TCP协议中,校验和用于验证TCP首部和数据字段在传输过程中是否被篡改或损坏。具体来说,TCP在发送端计算校验和,并将其放置在TCP首部中。接收端在收到数据后,也计算接收到的数据的校验和。如果接收端计算得到的校验和与发送端放置在TCP首部的校验和不一致,说明在传输过程中出现了错误。校验和的计算通常涉及对数据的各个字节进行求和,并将结果取反。这样,接收端在计算校验和时,如果校验和无误,那么计算结果加上接收到的校验和应该得到一个特定的值(通常为全1)。如果有任何一位发生变化,那么计算结果加上接收到的校验和就不会得到全1。这种方式简单而有效,可以检测到许多常见的传输错误。需要注意的是,校验和主要用于检测传输过程中的错误,而不是为了安全或加密目的。在TCP中,对于数据的保密性和完整性更高的要求通常由TLS/SSL等其他机制来处理。
紧急指针:紧急指针(Urgent Pointer)是用于指示紧急数据的位置的字段。这个字段的存在允许发送端发送紧急数据,并在接收端提供一种快速处理的机制。紧急数据通常是在传输过程中需要立即处理的数据,例如紧急的控制信息。需要注意的是,紧急指针的使用需要双方协商支持,并且它的应用相对较少。在实际网络通信中,更常见的是通过TLS/SSL等更高层次的安全协议来处理敏感的紧急数据。
三次握手机制
为什么要三次握手?
确保通信的可靠性和防止已失效的连接请求达到服务端
三次握手的作用分别可以这样理解:
第一次握手:客户端请求建立连接,发送一个带有 SYN(Synchronize)标志位的数据包给服务器,并选择一个初始序列号。客户端进入 SYN-SENT 状态,此时客户端并没有确认服务端的接收能力,服务端确认客户端发送正常自己接收正常。
第二次握手:服务器收到客户端的 SYN 包后,回复一个带有 SYN 和 ACK 标志位的数据包。服务器也会选择一个自己的初始序列号。服务器进入 SYN-RECEIVED 状态。此时,服务器确认了客户端的接收能力。客户端能确认自己和服务器接收和发送都正常,服务器此时能确认自己收发正常,但是只能知道客户端发送正常。
第三次握手:客户端收到服务器的 SYN+ACK 包后,发送一个带有 ACK 标志位的数据包给服务器。此时,客户端和服务端都能确认对面接收和发送正常。
所以三次握手缺一不可!
数据传输
当连接建立完成后,数据的传输过程是基于字节流的。TCP 将要传输的数据划分为合适的大小的数据段,每个数据段都有一个序列号。以下是 TCP 数据传输的一般过程:
-
数据段划分: 将要传输的数据划分为适当大小的数据段。数据段的大小通常由操作系统和网络条件共同决定。
-
序列号: 每个数据段都有一个序列号,用于标识该数据段的起始位置。序列号是一个32位的数字,表示相对于连接建立时的初始序列号的偏移量。
-
确认号: 收到数据的一方会发送确认号,表示它期望下一个接收到的字节是发送方序列号加上数据长度。确认号用于通知发送方数据已经成功接收。
-
滑动窗口: 滑动窗口机制用于流量控制。接收方会告知发送方它的接收窗口大小,即还能接收多少字节的数据。发送方根据接收方的窗口大小来控制发送的数据量,以避免超出接收方的处理能力。
-
超时重传: 如果发送方发送了一个数据段并在一定时间内没有收到对应的确认,它会认为数据可能丢失或损坏,然后触发超时重传机制,重新发送该数据段。
-
流量控制: 流量控制确保发送方不会发送过多的数据导致接收方无法处理。滑动窗口和接收方的确认一起实现了流量控制。
-
拥塞控制: 拥塞控制用于防止网络拥塞。TCP 通过检测网络的拥塞程度来调整发送速率。如果检测到网络拥塞,发送方会减缓发送速率。
-
重复消除: 接收方会检测并消除重复的数据,确保上层协议收到的是正确的、无重复的数据。
-
有序传输: TCP 保证数据的有序传输,即数据按照发送的顺序在接收方按序交付。
这些机制和步骤共同确保了 TCP 数据传输的可靠性和有序性。通过序列号、确认号、滑动窗口等机制,TCP 能够在不可靠的网络中提供可靠的、有序的数据传输服务。
关闭连接的四次挥手
为什么要四次挥手?
四次挥手时TCP连接时两端可以同时接受和发送数据,因此,每个端都必须要单独进行关闭。主要目的也是为了可靠的通信。
第一次挥手,客户端告诉没有数据发送了,但是还能接受服务端发来的数据。
第二次挥手,服务端告诉客户端你的意思我知道了,但是我还能发送数据给你,整个连接就处于半关闭了。
第三次挥手,服务端数据发送完毕,告诉客户端我也可以关闭连接了。
·第四次挥手,你的数据我接受完了,都关闭连接吧。
这四个步骤形成了TCP连接的完整关闭过程。这个过程确保了数据的可靠传输和连接的优雅关闭。在实际的网络通信中,这个过程可能会受到一些优化和调整,但基本的四次挥手过程是保持连接可靠关闭的核心。
可能得到面试问题:
为什么第二次挥手和第三次挥手不合成一个?
这是因为tcp是一个面向连接的协议,他的报文结构中的控制标记一次通信只能有一个值,不能存在多个。
1.5 UDP协议
UDP(User Datagram Protocol,用户数据报协议)是一种简单的面向无连接的传输层协议。与TCP(Transmission Control Protocol,传输控制协议)不同,UDP不提供可靠性、流控制或错误恢复。相反,它更注重在网络上传输数据的速度和效率。
特点
-
面向无连接: UDP是一种无连接的协议,通信的双方在发送数据之前不需要建立连接。这意味着通信双方不需要在传输数据之前进行握手过程,因此通信开销较小。
-
不提供可靠性: 与TCP不同,UDP不提供数据传输的可靠性。它不保证数据的到达、顺序和完整性。因此,应用程序需要自行处理数据的错误和丢失。
-
简单: UDP协议的设计相对简单,没有TCP那么复杂的连接管理和流控制机制。这使得UDP在某些场景下更适合,尤其是对于实时性要求较高的应用。
-
适用于实时应用: 由于UDP不引入较大的延迟和复杂性,它常被用于实时性要求较高的应用,例如语音通信、视频流传输和在线游戏。
-
报文形式: UDP通过数据报(Datagram)的形式传输数据。每个UDP数据报都是独立的,没有先后顺序,因此一个UDP报文的到达不影响后续报文的传输。
-
支持多播和广播: UDP支持多播(Multicast)和广播(Broadcast)通信,允许数据一次发送到多个目标。
-
没有拥塞控制: UDP不具备TCP中的拥塞控制机制,因此在网络拥塞的情况下,UDP的性能可能不如TCP稳定。
UDP适用于那些可以容忍一定数据丢失,但追求低延迟和高效传输的应用场景。在需要可靠性、顺序传输和错误恢复的情况下,通常会选择使用TCP。
报文结构
与TCP对比
TCO | UDP |
面向连接 | 无连接 |
提供可靠性保证 | 不可靠 |
慢 | 快 |
资源占用多 | 资源占用少 |
TCP类似打电话,双方建立连接后才能够说话,可以确保双方能听到各自的声音
UDP类似发短信,不是一种面向连接的服务,你随时可以发送短信,但是不能确保对方及时收到
二、Java中的网络编程Socket
1.三个套接字
-
数据报类型套接字(SOCK_DGRAM):
- 这种套接字类型是面向UDP(User Datagram Protocol,用户数据报协议)的接口。UDP是一种无连接的、不可靠的传输协议,适用于一些对实时性要求高,但可以容忍一定数据丢失的应用场景。数据报套接字适用于通过UDP进行通信的网络应用程序。
-
流式套接字(SOCK_STREAM):
- 这种套接字类型是面向TCP(Transmission Control Protocol,传输控制协议)的接口。TCP是一种面向连接、可靠的传输协议,适用于对数据可靠性和有序性有严格要求的应用场景,如文件传输、Web浏览等。流式套接字适用于通过TCP进行通信的网络应用程序。
-
原始套接字(SOCK_RAW):
- 这种套接字类型是面向底层网络层协议的接口,如IP(Internet Protocol,因特网协议)和ICMP(Internet Control Message Protocol,因特网控制消息协议)。原始套接字提供了对协议头的直接访问,允许程序直接处理网络层数据。这种类型的套接字通常用于实现一些特殊的网络工具和应用,而不是一般的应用程序通信。
2.主要Socket API及其调用过程
3.示例
clien:
mport java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",9999);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
String msg=scanner.nextLine()+"\r\n";
outputStream.write(msg.getBytes());
outputStream.flush();
scanner.close();
socket.close();
}
}
server:
package com.syc.one.test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) throws IOException {
//创建服务器,监听9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//在程序即将关闭时,关闭一个 serverSocket 实例
Runtime.getRuntime().addShutdownHook(new Thread(()->{
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
System.out.println("服务器启动成功");
while (true){
//监听与这个套接字的连接并接受他
Socket request = serverSocket.accept();
System.out.println(request.toString());
try {
//获取字节输入流
InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg,rsp="";
while ((msg=bufferedReader.readLine()) != null){
if(msg.length()==0) break;
rsp += msg;
System.out.println(msg);
}
System.out.println("服务器收到数据"+rsp);
}catch (Exception e){
System.out.println(e);
}finally {
request.close();
}
}
}
}
4.模拟HTTP服务
当我们使用浏览器给服务器发送数据时(访问:https://localhost:9999)控制台会打印以下信息:
现在我们要给浏览器正确响应数据,修改服务器部分代码,如下:
try {
//获取字节输入流
InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg,rsp="";
while ((msg=bufferedReader.readLine()) != null){
if(msg.length()==0) break;
rsp += msg;
System.out.println(msg);
}
System.out.println("服务器收到数据"+rsp);
OutputStream outputStream = request.getOutputStream();
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
outputStream.write("Content-Length 11\r\n\r\n".getBytes());
outputStream.write("Hellow word".getBytes());
outputStream.flush();
}catch (Exception e){
添加了响应内容,此时浏览器页面如下:
5.HTTP状态码
1XX(临时响应):
表示临时响应并需要请求者继续执行操作的状态代码
2XX(请求成功):表示成功处理了请求
3XX(重定向):
表示完成请求,需要进一步操作
4XX(请求错误):
这些错误码表示请求可能出错,妨碍服务器进行处理
5XX(服务器内部出错):
请求发送成功,但是服务器内部出现错误。
6.并发增大了
这里就涉及到了BIO、AIO、NIO等知识,请看我的另一篇文章
三、Netty概述
Netty是一个用于快速开发高性能网络应用程序的异步的、基于事件驱动的网络应用框架。它是一个基于Java NIO(New I/O)的框架,专注于提供可扩展的、高性能的网络通信。他极大的简化了TCP客户端和UDP客户端和服务器开发等网络编程。(官网:Netty: Home)
1.原生NIO存在的问题
1) NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、 SocketChannel、 ByteBuffer等。
2)需要具备其他的额外技能:要熟悉Java 多线程编程,因为 NIO 编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO 程序。
3)开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
4)JDKNIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致Selector 空轮询,最终导致CPU 100%。直到 JDK 1.7版本该问题仍旧存在,没有被根本解决。
2.netty的优点
Netty 对JDK自带的NIO的API 进行了封装,解决了上述问题。
1) 设计优雅:适用于各种传输类型的统一API阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型–单线程,一个或多个线程池.
2)使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty 3.x)或6 (Netty 4.x)就足够了。
3)高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
4)安全:完整的SSL/TLS 和 StartTLS支持。
5)社区活跃、不断更新:社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入
3.应用场景
互联网行业 :在分布式系统中,各个节点之间需要远程调用,高性能的RPC框架(一种用于实现远程过程调用的技术和框架。它允许在网络上的不同计算机上的程序之间进行通信,就像调用本地方法一样,而不必关心底层的网络通信细节。)必不可少,Netty作为高性能的通信框架,往往作为基础的通信组件被这些RPC框架所使用。
典型的应用有阿里的Dubbo,使用Dubbo协议进行节点间的通信,Dubbo协议默认使用Netty作为基础通信组件。
游戏行业:Netty提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登录服务器。地图服务器之间可以方便的通过Netty进行高性能通信。
四、Netty高性能架构设计
1.线程模型基本介绍
1)不同的线程模式,对程序的性能有很大影响,为了搞清Netty 线程模式,我们来系统的讲解下各个线程模式,最后探讨Netty线程模型的优越性。
2)目前存在的线程模型有:传统阻塞IO服务模型Reactor模式
3)根据Reactor的数量和处理资源池线程的数量不同,有三种典型的实现:单Reactor单线程、单Reactor多线程、主从Reactor多线程。
4)Netty的线程模型主要基于主从Reactor多线程模型做了一定的改进。其中,主从Reactor多线程模型包含多个Reactor,这使得Netty能够更高效地处理并发连接和I/O操
2.传统阻塞IO服务模型
1.工作原理图
2.问题:
并发增大时,服务器压力会很大,因为一个请求就会创建一个线程,连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程浪费。
3.Reactor模式
1.传统阻塞Io的解决
1)基于I/O复用模型:多个连接共用一个阻塞对象,程序只需要在一个阻塞对象处等待,无需阻塞等待所有连接。当某个连接有数据需要处理时,操作系统通知应用程序,线程从阻塞状态返回,开始处理业务,Reactor对应的叫法是:1.反应器模式2分发者模式3通知者模式
2)基于线程池,不必再为每个连接创建新线程,避免了服务器资源耗尽。
IO复用+线程池就是Reactor模式的基本设计思想
2.Reactor的核心组成
1) Reactor: Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
2) Handlers:处理程序执行IO 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应IO事件,处理程序执行非阻塞操作。
3.单Reactor单线程模式
代码
说明信息:
1.Reactor类内置属性Selector,并在构造函数中完成了服务器的初始化,如:创建服务器通道对象ServerSocketChannel、绑定端口、设置关注连接事件等。
2.Reactor实现了Runnable接口,在重写的run方法中监听事件的发生,并通过dispatch(进行任务的派发),由于我们在Reactor的构造函数中,第一次添加的附加对象是:Acceptor:
selectionKey.attach(new Acceptor(selectionKey));所以此时调用的是Acceptor类的run()方法,在这个run()方法中,创建与客户端的连接通道,注册到selector上,并设置关注读事件,3.Acceptor类的run()方法最后创建了Handler对象,在他的构造函数中将自己附加给了SelectionKey,此时代码会回到Reactor的run方法内继续监听事件的发生,当客户端发起写事件时,监听到,然后调用dispatch方法,在这里取出附加对象Handler,调用他的run方法,处理写事件
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class ReactorSignDemo {
public static void main(String[] args) throws IOException {
ReactorSignDemo reactorSignDemo = new ReactorSignDemo();
Reactor reactor = reactorSignDemo.new Reactor();
reactor.run();
}
class Reactor implements Runnable {
final Selector selector;
public Reactor() throws IOException {
selector=Selector.open();
//创建服务端channel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//channel注册到selector上,并注册accept时间
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);//0表示该通道不关注任何特定的事件类型
//表示当有新的客户端连接时,Selector 将通知该 SelectionKey。
selectionKey.interestOps(SelectionKey.OP_ACCEPT);// 设置关注 OP_ACCEPT 事件。
//将一个附加对象(Acceptor 的实例)关联到 SelectionKey。Acceptor 实例,它是一个实现了 Runnable 接口的类,用于处理新连接的接受操作。
selectionKey.attach(new Acceptor(selectionKey));
//绑定端口启动服务
serverSocketChannel.bind(new InetSocketAddress(8088));
System.out.println("服务器启动成功");
}
@Override
public void run() {
while (true){//在run方法中遍历事件
try {
//阻塞等待事件的发生,然后通过迭代器处理已选择的键集合,最终调用 dispatch 方法派发事件。
int num = selector.select();
if (num==0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
dispatch(key);//通过dispatch方法进行事件的派发
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 从 SelectionKey 中获取附加的对象(在这里是 Acceptor 实例),然后调用其 run 方法。用于派发处理器的运行。
private void dispatch(SelectionKey key){
//取出上面绑定的附加对象,也就是Acceptor,调用他的run方法
Runnable attachment =(Runnable) key.attachment();
if(attachment!=null){
attachment.run();
}
}
}
class Acceptor implements Runnable{
final SelectionKey key;
final ServerSocketChannel serverSocketChannel;
public Acceptor(SelectionKey key ){
this.key=key;
this.serverSocketChannel= (ServerSocketChannel) key.channel();
}
//处理新的客户端连接,设置连接的通信通道为非阻塞模式,并在 Selector 上注册关注 OP_READ 事件的 SocketChannel,
// 最后创建一个新的 Handler 处理器。
@Override
public void run() {
try {
//接收到新的客户端连接,绑定到select上
System.out.println("收到客户端新连接");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞模式,这个是与连接上的客户端的通信管道
//将新创建的 SocketChannel 注册到 Selector 上,关注 OP_READ 事件,表示该通道已准备好进行读取操作。然后获取到他的SelectionKey对象
SelectionKey register = socketChannel.register(key.selector(), SelectionKey.OP_READ);
//接收完客户端连接之后,处理读写事件
new Handler(register);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//处理读写事件处理器
class Handler implements Runnable{
final SocketChannel socketChannel;
final SelectionKey key;
int readIng=0,writeIng=1;
int state=readIng;
public Handler(SelectionKey key ){
this.key=key;
this.socketChannel =(SocketChannel) key.channel();
key.attach(this);
}
//处理读写事件,根据状态进行读或写操作,包括读取客户端数据和向客户端发送数据
@Override
public void run() {
//处理读写
try {
if(state==readIng){
System.out.println("开始读数据。。");
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
socketChannel.close();
//取消对应的选择键,即从 Selector 的键集合中移除该键。
key.cancel();
} else if (bytesRead > 0) {
buffer.flip();//切换读模式
System.out.println("读到数据:" + new String(buffer.array(), 0, bytesRead));
// 处理完数据后,清除OP_READ事件,避免重复触发
state=writeIng;
//将 OP_WRITE 事件添加到 SelectionKey 的兴趣操作集合中
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
}
}else if(state==writeIng){
System.out.println("给客户端返回数据");
ByteBuffer byteBuffer = ByteBuffer.wrap("我是服务器发给客户端的信息".getBytes());
socketChannel.write(byteBuffer);
//写操作之后吗,应该清除op——write
state = readIng;
//将 OP_WRITE 事件从 SelectionKey 的兴趣操作集合中移除
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
客户端:
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class SocketChannelClient {
public static void main(String[] args) throws IOException {
// 打开客户端通道
SocketChannel socketChannel = SocketChannel.open();
// 配置为非阻塞模式
socketChannel.configureBlocking(false);
// 连接到服务器
socketChannel.connect(new InetSocketAddress("localhost", 8088));
while (!socketChannel.finishConnect()) {
// 非阻塞模式下,需要轮询等待连接完成
Thread.yield();
}
// 向服务器发送消息
Scanner scanner=new Scanner(System.in);
System.out.println("请输入:");
//wrap:将字节数组包装到缓冲区
ByteBuffer buffer = ByteBuffer.wrap(scanner.nextLine().getBytes());
//判断缓冲区是否有数据,有的话通过通道发送给服务器
while (buffer.hasRemaining()){
socketChannel.write(buffer);
}
// 读取服务器的响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen()&&socketChannel.read(responseBuffer) != -1){
//长连接情况下需要判断数据读取是否结束了,这里只做简单判断
if(responseBuffer.position()>0) break;
}
responseBuffer.flip();//切换读模式
System.out.println("收到服务器发送数据:"+new String(responseBuffer.array(),0,responseBuffer.remaining()));
// 关闭客户端通道
socketChannel.close();
}
}
整个程序只有一个
Reactor
线程来监听事件、派发任务,并处理连接、读写操作。这是一个经典的 Reactor 模式的单线程实现,适用于处理并发连接较少、每个连接处理时间较短的场景。无法发挥多核cpu的性能,当执行Handler类的run方式时,无法处理其他的连接请求,造成其他客户端等待很久甚至。
可靠性问题:线程意外终止,会导致整个服务不可用。
4.单Reactor多线程模式
与单Reactor单线程模式基本一致,在处理业务时使用线程池处理读写业务。
流程图:
服务器代码:
package com.syc.one.test;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolReactorSignDemo {
public static void main(String[] args) throws IOException {
Reactor reactor = new Reactor(8088);
reactor.run();
}
static
class Reactor implements Runnable {
final Selector selector;
public Reactor(int port) throws IOException {
selector=Selector.open();
//创建服务端channel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//channel注册到selector上,并注册accept时间
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);//0表示该通道不关注任何特定的事件类型
//表示当有新的客户端连接时,Selector 将通知该 SelectionKey。
selectionKey.interestOps(SelectionKey.OP_ACCEPT);// 设置关注 OP_ACCEPT 事件。
//将一个附加对象(Acceptor 的实例)关联到 SelectionKey。Acceptor 实例,它是一个实现了 Runnable 接口的类,用于处理新连接的接受操作。
selectionKey.attach(new Acceptor(selectionKey));
//绑定端口启动服务
serverSocketChannel.bind(new InetSocketAddress(port));
System.out.println("服务器启动成功");
}
@Override
public void run() {
while (true){//在run方法中遍历事件
try {
//阻塞等待事件的发生,然后通过迭代器处理已选择的键集合,最终调用 dispatch 方法派发事件。
int num = selector.select();
if (num==0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
dispatch(key);//通过dispatch方法进行事件的派发
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 从 SelectionKey 中获取附加的对象(在这里是 Acceptor 实例),然后调用其 run 方法。用于派发处理器的运行。
private void dispatch(SelectionKey key){
//取出上面绑定的附加对象,也就是Acceptor,调用他的run方法
Runnable attachment =(Runnable) key.attachment();
if(attachment!=null){
attachment.run();
}
}
}
static class Acceptor implements Runnable{
final SelectionKey key;
final ServerSocketChannel serverSocketChannel;
public Acceptor(SelectionKey key ){
this.key=key;
this.serverSocketChannel= (ServerSocketChannel) key.channel();
}
//处理新的客户端连接,设置连接的通信通道为非阻塞模式,并在 Selector 上注册关注 OP_READ 事件的 SocketChannel,
// 最后创建一个新的 Handler 处理器。
@Override
public void run() {
try {
//接收到新的客户端连接,绑定到select上
System.out.println("收到客户端新连接");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞模式,这个是与连接上的客户端的通信管道
//将新创建的 SocketChannel 注册到 Selector 上,关注 OP_READ 事件,表示该通道已准备好进行读取操作。然后获取到他的SelectionKey对象
SelectionKey register = socketChannel.register(key.selector(), SelectionKey.OP_READ);
//接收完客户端连接之后,处理读写事件
new Handler(register);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//处理读写事件处理器
static class Handler implements Runnable{
final SocketChannel socketChannel;//与客户端的连接通道
final SelectionKey key;
final int MAXSIZE=1024;
ByteBuffer input=ByteBuffer.allocate(MAXSIZE);
//线程池
static final ExecutorService service=MyThreadPool.service;
public Handler(SelectionKey key ){
this.key=key;
this.socketChannel =(SocketChannel) key.channel();
key.attach(this);
}
//处理读写事件,根据状态进行读或写操作,包括读取客户端数据和向客户端发送数据
@Override
public void run() {
//处理读写
try {
if(key.isReadable()){
read();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void read() throws IOException {
System.out.println("开始读数据。。");
int read = socketChannel.read(input);
if(inputIsComplete()){//读取数据完成
input.flip();//转换为写模式
service.execute(new Processer(input,key,read));
input.clear();
}
}
boolean inputIsComplete(){
return input.position()>0;
}
}
static class Processer implements Runnable {
ByteBuffer byteBuffer;
SelectionKey key;
int size;
public Processer(ByteBuffer byteBuffer,SelectionKey key,int size){
this.byteBuffer=byteBuffer;
this.key=key;
this.size=size;
}
@Override
public void run() {
//在这里处理业务,如保存到数据库
System.out.println("读到客户端发来的信息:"+new String(byteBuffer.array(),0,size));
//给客户端发送数据
ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put("我是服务器AA给客户端发送数据".getBytes());
try {
allocate.flip();
System.out.println("创建对象Sender");
new Sender(allocate,key).run();
}catch (Exception e){
System.out.println(e);
}
}
}
static class Sender implements Runnable{
final SocketChannel socketChannel;
final Object oldObject;
SelectionKey key;
ByteBuffer byteBuffer;
public Sender(ByteBuffer byteBuffer,SelectionKey key){
this.byteBuffer=ByteBuffer.wrap(byteBuffer.array(),0,byteBuffer.limit());
this.key=key;
socketChannel=(SocketChannel)key.channel();
//注册写事件
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
oldObject=key.attach(this);//返回上一个附加对象
}
@Override
public void run() {
System.out.println(key.isReadable());
System.out.println(key.isWritable());
try {
System.out.println("给客户端返回数据。。。");
socketChannel.write(byteBuffer);
if(outputIsComplete()){
byteBuffer.clear();
key.attach(oldObject);
key.interestOps(key.interestOps() & ~ SelectionKey.OP_WRITE);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public boolean outputIsComplete(){
return byteBuffer.position()==byteBuffer.limit();
}
}
}
1)优点:可以充分的利用多核cpu的处理能力
2) 缺点:多线程数据共享和访问比较复杂,reactor 处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈.
5.主从Reactor多线程
1) Reactor主线程MainReactor对象通过select 监听连接事件,收到事件后,通过Acceptor 处理连接事件
2)当Acceptor处理连接事件后,MainReactor将连接分配给SubReactor
3) subreactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理4)当有新事件发生时,subreactor就会调用对应的 handler 处理
5) handler通过read 读取数据,分发给后面的 worker 线程处理6) worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
7) handler 收到响应的结果后,再通过send将结果返回给client
8) Reactor主线程可以对应多个Reactor子线程,即 MainRecator可以关联多个SubReactor
代码见:
6.Reactor模式理解
1)单 Reactor单线程,前台接待员和服务员是同一个人,全程为顾客服
2)单 Reactor多线程,1个前台接待员,多个服务员,接待员只负责接待
3)主从 Reactor 多线程,多个前台接待员,多个服务生
4.Netty模型
1.运行流程图
1) Netty抽象出两组线程池 BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写
2) BossGroup和WorkerGroup类型都是NioEventLoopGroup
3) NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop4) NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个Selector ,用于监听绑定在其上的 socket的网络通讯(也就是和客户端的通信通道)
5) NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop6)每个Boss NioEventLoop 循环执行的步骤有3步
. 轮询accept事件
.处理accept 事件 ,与client建立连接﹐生成NioScocketChannel,并将其注册到某个worker NIOEventLoop 上的selector
.处理任务队列的任务,即runAllTasks
7)每个Worker NIOEventLoop 循环执行的步骤轮询read, write事件处理i/o事件,即read , write事件,在对应NioScocketChannel处理处理任务队列的任务,即runAllTasks
8)每个Worker NIOEventLoop处理业务时,会使用pipeline(管道), pipeline中包含了channe
,即通过pipeline可以获取到对应通道,管道中维护了很多的处理器
导入依赖
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.42.Final</version> </dependency>
代码:
服务器
package com.syc.one.test4;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) {
//创建BossGroup 和WorkerGroup
//1.创建两个线程组bossGroup和 workerGroup
//2.bossGroup只是处理连接请求,真正的和客户端业务处理,会交给 workerGroup完成
// 3.两个都是无限循环
//4.bossGroup和 workerGroup含有的子线程(NioEventLoop)的个数默认为cpu核数*2
NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup=new NioEventLoopGroup();//不指定的话个数默认为cpu核数*2
try{
ServerBootstrap bootstrap = new ServerBootstrap();
//链式编程设置配置信息
bootstrap.group(bossGroup,workerGroup)//设置两个线程组
.channel(NioServerSocketChannel.class)//使用NioServerSocketChannel作为服务器通道的实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列的连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动的连接个数
.childHandler(new ChannelInitializer<SocketChannel>() {//使用匿名方式创建一个通道测试对象
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//可以使用一个集合管理SocketChannel,推送消息时将业务加入到每个channel对应的NIOEventLoop的任务队列上
//如:ch.eventLoop().schedule()
System.out.println("客户端通道"+ch.hashCode());
ch.pipeline().addLast(new NettyServerHandler());
ch.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("服务器启动完成。。。");
ChannelFuture cf = bootstrap.bind(8899).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务器处理器
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.EventExecutorGroup;
//ChannelInboundHandlerAdapter:这是一个基础的入站处理器,你需要手动管理资源的释放。它的 channelRead 方法接收到的消息需要你自己负责释放
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg){
System.out.println("服务器读取线程"+Thread.currentThread().getName());
System.out. println("server 上下文对象 ="+ ctx);
System.out. println("看看channel 和 pipeline 的关系");//相互包含,你中有我我中有你
Channel channel =ctx.channel();
ChannelPipeline pipeline =ctx.pipeline();//l本质是一个双向链接,出站入站
//将 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){
//writeAndFlush是 write + flush
//将数据写入缓存并刷新
//一般要对发送数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端酱",CharsetUtil.UTF_8));
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
客户端
package com.syc.one.test4;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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) {
//客户端事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象,注意客户端使用的是Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)//设置线程组
.channel(NioSocketChannel.class)//设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());//加入自己的处理器
}
});
System.out.println("客户端ok");
//启动客户端去连接服务器
ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
group.shutdownGracefully();
}
}
}
客户端处理器
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.EventExecutorGroup;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//与服务器通道就绪触发方法
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("客户端上下文" + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("你好服务器", CharsetUtil.UTF_8));
}
//通道有读取事件发生触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回发的数据是" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器地址是" + ctx.channel().localAddress());
System.out.println("服务器地址是" + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
前面第三第四点所说证明:
2.任务队列(TaskQueue)中的Task的3中典型使用场景
1)用户程序自定义的普通任务
2)用户自定义定时任务
3)非当前Reactor 线程调用Channel 的各种方法。如:例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景。最终的 Write会提交到任务队列中后被异步消费
示例:
package com.syc.one.test4;
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;
import java.util.concurrent.TimeUnit;
public class NettyServerHandlerTask extends ChannelInboundHandlerAdapter {
//读取数据
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg){
//比如这里我们有一个非常耗时长的业务->异步执行>提交该channel 对应的NIOEventLoop 的 taskQueue中,
//解决方案1用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
ctx.writeAndFlush(Unpooled.copiedBuffer("你好客户端",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//解决方案2:用户自定义定时任务,,该任务提交到scheduledTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(7000);
ctx.writeAndFlush(Unpooled.copiedBuffer("你好客户端2",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},5, TimeUnit.SECONDS);
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx){
//writeAndFlush是 write + flush
//将数据写入缓存并刷新
//一般要对发送数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端酱",CharsetUtil.UTF_8));
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
3.总结
1) Netty 抽象出两组线程池,BossGroup专门负责接收客户端连接,WorkerGroup专门负责网络读写操作。
2)NioEventLoop表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个selector,用于监听绑定在其上的 socket 网络通道。
3) NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理>编码->发送,始终由IO 线程 NioEventLoop负责NioEventLoopGroup下包含多个NioEventLoop
每个NioEventLoop中包含有一个Selector,一个taskQueue每个NioEventLoop 的Selector 上可以注册监听多个NioChannel
每个NioChannel只会绑定在唯一的NioEventLoop 上
每个NioChannel 都绑定有一个自己的 ChannelPipeline
5.异步模型
1.基本介绍
1)异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
2)Netty 中的IO操作是异步的,包括Bind、Write、Connect 等操作会简单的返回一个ChannelFuture。如图:上面客户端的代码
3)调用者并不能立刻获得结果,而是通过Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得
IO操作结果
4) Netty的异步模型是建立在future和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun返回显然不合适。那么可以在调用fun 的时候,立马返回一个Future,后续可以通过Future去监控方法 fun的处理过程(即: Future-Listener 机制)
2.Future说明
1)表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等.
2)ChannelFuture是一个接口 :public interface ChannelFuture extends Future<Void>
我们可以添加监听器,当监听的事件发生时,就会通知到监听器.
3.工作原理图
说明:
1)在使用Netty进行编程时,拦截操作和转换出入站数据只需要您提供calback或利用future即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码。
2) Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来
4.Future-Listener机制
1)当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
2)常见有如下操作
√通过isDone方法来判断当前操作是否完成;
√通过isSuccess方法来判断已完成的当前操作是否成功;
√通过getCause方法来获取已完成的当前操作失败的原因;
√通过isCancelled方法来判断已完成的当前操作是否被取消;
√通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知指定的监听器
示例:
//启动客户端去连接服务器
ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync();
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("连接服务器成功");
}else {
System.out.println("连接服务器失败");
}
}
});
6.快速入门实例-HTTP服务
服务器:
package com.syc.one.test5;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class TestServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
ChannelFuture sync = bootstrap.bind(8899).sync();
sync.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (sync.isSuccess()) {
System.out.println("服务器启动成功");
}
}
});
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.syc.one.test5;
import io.netty.channel.ChannelHandler;
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 socketChannel) throws Exception {
//向管道加入处理器
//得到管道
ChannelPipeline pipeline = socketChannel.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.syc.one.test5;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
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
*/
//SimpleChannelInboundHandler:泛型类,需要指定处理的消息类型
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//读取客户端数据 你只需要专注于业务逻辑处理即可。在处理完消息后,不需要手动释放资源。
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断msg是不是 httpRequest 请求
if (msg instanceof HttpRequest) {
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode()
+ "TestHtpServerHandler 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);
}
}
}
五、Netty的核心组件
1.Bootstrap、ServerBootstrap
1) Bootstrap意思是引导,一个Netty应用通常由一个 Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中 Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
2)常见方法有:
1.public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) ;用于配置服务器端的事件循环组(EventLoop)。parentGroup用于处理连接,childGroup处理读写等2.public B group(EventLoopGroup group) ;该方法用于客户端,设置一个事件循环组
3.public B channel(Class<? extends C> channelClass);指定服务器用于接收连接的通道类型
4.public <T> B option(ChannelOption<T> option, T value);配置传输通道(serverChaneel)的一些底层参数,以满足特定需求
5.public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value);它用于设置子通道(已经建立连接的通道)的选项。
6.public ServerBootstrap childHandler(ChannelHandler childHandler) ;设置业务处理器
7.public ChannelFuture bind(int inetPort)设置服务器端口号
8.public ChannelFuture connect(String inetHost, int inetPort)客户端连接服务器
2.Future、ChannelFuture
Netty中所有的IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
常见的方法有
Channel channel(),返回当前正在进行IO操作的通道
ChannelFuture sync(),等待异步操作执行完毕
3.Channel
1) Netty 网络通信的组件,能够用于执行网络I/O操作。
2)通过Channel可获得当前网络连接的通道的状态
channel.isOpen();
3)通过Channel可获得网络连接的配置参数(例如接收缓冲区大小)ChannelConfig config = channel.config();
// 例如,获取接收缓冲区大小 int receiveBufferSize = config.getRecvByteBufAllocator().compositeMaxComponents();
4)Channel 提供异步的网络IO 操作(如建立连接,读写,绑定端口),异步调用意味着任何TO调用都将立即返回,并且不保证在调用结束时所请求的IO操作已完成
5)调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture 上,可以I/O 操作成功、失败或取消时回调通知调用方
6)支持关联IO操作与对应的处理程序
7)不同协议、不同的阻塞类型的连接都有不同的Channel类型与之对应,常用的Channel类型:NioSocketChannel,异步的客户端TCP Socket 连接。
NioServerSocketChannel,异步的服务器端TCP Socket连接。
NioDatagramChannel,异步的UDP连接。
NioSctpChannel,异步的客户端Sctp 连接。
NioSctpServerChannel,异步的 Sctp服务器端连接,这些通道涵盖了UDP和 TCP网络IO以及文件IO。
4.Selector
1) Netty基于Selector对象实现IO 多路复用,通过Selector一个线程可以监听多个连接的Channel事件。
2)当向一个Selector 中注册Channel后,Selector 内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的IO 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel。
5.ChannelHandler及其实现类
1) ChannelHandler是一个接口,处理IO事件或拦截IO 操作,并将其转发到ChannelPipeline(业务处理链)中的下一个处理程序。
2)ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类
3)ChannelHandler及其实现类一览图4)我们经常需要自定义一个 Handler类去继承ChannelInboundHandler Adapter,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法(加粗字体):
//在通道被注册到 EventLoop 时触发。 public void channelRegistered(ChannelHandlerContext ctx) ; //在通道从EventLoop 中注销时触发 public void channelUnregistered(ChannelHandlerContext ctx); //通道连接就绪触发 public void channelActive(ChannelHandlerContext ctx) ; //连接关闭触发 public void channelInactive(ChannelHandlerContext ctx) ; //有数据可读时触发 public void channelRead(ChannelHandlerContext ctx, Object msg); //数据读取完触发 public void channelReadComplete(ChannelHandlerContext ctx) ; //在用户自定义事件被触发时调用。用户可以通过 ChannelPipeline 发送自定义事件,这个方法用于处理这些事件。 public void userEventTriggered(ChannelHandlerContext ctx, Object evt); //在通道的可写状态发生改变时触发,通常用于处理高水位标记的变化。 public void channelWritabilityChanged(ChannelHandlerContext ctx) ; //在处理过程中出现异常时触发,允许对异常进行处理或者关闭连接。 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) ;
6.Pipeline和 ChannelPipeline
ChannelPipeline是一个重点:
1) ChannelPipeline是一个Handler 的集合,它负责处理和拦截inbound或者outbound 的事件和操作,相当于一个贯穿Netty的链。(也可以这样理解:ChannelPipeline是保存ChannelHandler 的List.用于处理或拦截Channel 的入站事件和出站操作)
2) ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互
3)在Netty中每个Channel都有且仅有一个ChannelPipeline 与之对应,它们的组成关系如下4)常用方法
//把一个业务处理类(handler)添加到链中的第一个位置
ChannelPipeline addFirst(ChannelHandler..handlers),
//把一个业务处理类(handler)添加到链中的最后一个位置
ChannelPipeline addLast(ChannelHandler. handlers),
7.ChannelHandlerContext
1)保存Channel 相关的所有上下文信息,同时关联一个ChannelHandler对象
2)即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler ,同时
ChannelHandlerContext 中也绑定了对应的 pipeline和 Channel 的信息,方便对ChannelHandler进行调用.3)常用方法
8.ChannelOption
1) Netty在创建Channel实例后,一般都需要设置 ChannelOption参数。
2)ChannelOption参数部分如下:
9 EventLoopGroup和其实现类NioEventLoopGroup
l) EventLoopGroup是一组EventLoop 的抽象,Netty 为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop 维护着一个Selector实例。
2) EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如: BossEventLoopGroup和WorkerEventLoopGroup。
3) 通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给 WorkerEventLoopGroup来进行IO 处理,如下图所示
10.Unpooled类
1) Netty提供一个专门用来操作缓冲区(即 Netty 的数据容器)的工具类
2)常用方法如下所示
//用于将给定的字符序列(
CharSequence
)按照指定的字符集(Charset
)编码为一个新的ByteBuf
对象。public static ByteBuf copiedBuffer(CharSequence string, Charset charset)使用示例:
//数据读取完毕 @Override public void channelReadComplete(ChannelHandlerContext ctx){ //writeAndFlush是 write + flush //将数据写入缓存并刷新 //一般要对发送数据进行编码 ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端酱",CharsetUtil.UTF_8)); }ByteBuf buf =(ByteBuf) msg; System.out.println("客户端发送消息是:"+ buf.toString(CharsetUtil.UTF_8));
11. Netty应用实例-群聊系统实例要求:
1)编写一个Netty群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以监测用户上线,离线,并实现消息转发功能
4)客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
5)目的:进一步理解 Netty非阻塞网络编程机制
代码:
这里拿上面的Netty模型处的代码的服务器处理器做修,实现了需求。
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.EventExecutorGroup;
import java.util.ArrayList;
import java.util.List;
//ChannelInboundHandlerAdapter:这是一个基础的入站处理器,你需要手动管理资源的释放。它的 channelRead 方法接收到的消息需要你自己负责释放
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
static int onLinePeople;
static List<SocketChannel> socketChannelList=new ArrayList<>();
//通道连接就绪触发连接就绪方法,配合连接关闭可以用于统计在线人数
@Override
public void channelActive(ChannelHandlerContext ctx){
onLinePeople+=1;
System.out.println("地址:"+ctx.channel().remoteAddress()+"用户上线");
System.out.println("当前在线人数为:"+onLinePeople);
SocketChannel channel =(SocketChannel) ctx.channel();
socketChannelList.add(channel);
}
@Override
public void channelInactive(ChannelHandlerContext ctx){
onLinePeople-=1;
System.out.println("地址:"+ctx.channel().remoteAddress()+"用户下线");
System.out.println("当前在线人数为:"+onLinePeople);
//移除下线的客户端通道
SocketChannel channel =(SocketChannel) ctx.channel();
for (SocketChannel socketChannel : socketChannelList) {
if(channel.equals(socketChannel)){
socketChannelList.remove(socketChannel);
}
}
}
//读取数据
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg){
//将任务提交到与 Channel 相关联的事件循环(EventLoop)中执行
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
//读取客户端发送的消息
ByteBuf buf =(ByteBuf) msg;
String s = buf.toString(CharsetUtil.UTF_8);
System.out.println("客户端地址"+ctx.channel().remoteAddress()+"用户发消息"+s);
//转发给其他客户端
SocketChannel channel =(SocketChannel) ctx.channel();
if(socketChannelList.size()>1){
//排除自己给其他客户端发消息
for (SocketChannel socketChannel : socketChannelList) {
if(!channel.equals(socketChannel)){
socketChannel.writeAndFlush(Unpooled.copiedBuffer(s, CharsetUtil.UTF_8));
}
}
}
}
});
}
// //数据读取完毕
// @Override
// public void channelReadComplete(ChannelHandlerContext ctx){
// //writeAndFlush是 write + flush
// //将数据写入缓存并刷新
// //一般要对发送数据进行编码
// ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端酱",CharsetUtil.UTF_8));
// }
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
上述代码,使用的是List维护的Channel,可以使用DefaultChannelGroup
//DefaultChannelGroup 是 Netty 中用于管理一组 Channel 的类,它可以方便地对多个 Channel 进行批量操作,如广播消息、批量关闭连接等 //GlobalEventExecutor.INSTANCE:使用全局共享的事件执行器 DefaultChannelGroup channelGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);DefaultChannelGroup进行维护,
12.Netty心跳检测机制案例
1)编写一个Netty 心跳检测机制案例,当服务器超过3秒没有读时,就提示读空闲
2)当服务器超过5秒没有写操作时,就提示写空闲
3)实现当服务器超过7秒没有读或者写操作时,就提示读写空闲
代码:
还在在之前的代码上做修改:服务器端添加Netty提供的空闲状态处理器,还有两个编码解码处理器,有了编码解码处理器,我们的自定义处理器里面的msg直接用就行,就不用自己编码解码了。
.childHandler(new ChannelInitializer<SocketChannel>() {//使用匿名方式创建一个通道测试对象
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//可以使用一个集合管理SocketChannel,推送消息时将业务加入到每个channel对应的NIOEventLoop的任务队列上
//如:ch.eventLoop().schedule()
ChannelPipeline pipeline = ch.pipeline();
//IdleStateHandler是Netty提供的空闲状态的处理器
//第一二三个参数分别表示多长时间没有读、写、读写后发送一个心跳检测包监测是否连接
pipeline.addLast(new IdleStateHandler(13,5,5, TimeUnit.SECONDS));
//加入编码解码器和自己的业务处理器
pipeline.addLast("decoder",new StringDecoder());
pipeline.addLast("encoder",new StringEncoder());
pipeline.addLast(new NettyServerHandler());
}
});
自己的处理器中添加方法:
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
//将evt向下转型IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress()+"--超时时间--"+eventType);System.out.println("服务器做相应处理..");
//如果发生空闲,我们关闭通道
//ctx.channel().close();
}
}
效果:
13 Netty通过WebSocket编程实现服务器和客户端长连接
实例要求:
l) Http协议是无状态的,浏览器和服务器间的请求响应一次,下一次会重新创建连接.
2)要求:实现基于webSocket 的长连接的全双工的交互
3)改变Http协议多次请求的约束,实现长连接了,服务器可以发送消息给浏览器
4)客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知5)运行界面
服务器代码:
package com.syc.one.testWebSocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//因为是http协议,所以添加http协议的编码解码器
pipeline.addLast(new HttpServerCodec());
//它主要用于处理分块写入(chunked write)的场景,特别是在写入大文件或大数据流时,可以将数据分成一系列的块进行异步写入
// ,以避免一次性写入过大的数据导致内存占用过高
pipeline.addLast(new ChunkedWriteHandler());
//1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚
//2.这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
pipeline.addLast(new HttpObjectAggregator(8192));
//WebSocketServerProtocolHandler核心功能是将 http协议升级为 ws协议,保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello2"));
//自定义处理器
pipeline.addLast(new MyWebSocketHandler());
}
});
ChannelFuture channelFuture = bootstrap.bind(8899).sync();
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("服务器启动成功");
}
}
});
}
}
自定义处理器
package com.syc.one.testWebSocket;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.EventExecutorGroup;
import java.time.LocalDateTime;
///这里TextWebSocketFrame类型,表示一个文本帧(frame)
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务器收到消息"+msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间为"+ LocalDateTime.now()));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx){
//id表示唯一的值,LongText是唯一的 ShortText不是唯一
System.out.println(" handlerAdded被调用"+ctx.channel().id().asLongText());
System.out.println(" handlerAdded被调用"+ctx.channel().id().asShortText());
}
@Override//从 ChannelPipeline 中移除时被调用。它表示当前 ChannelHandler 被移除并即将不再参与后续的事件处理。
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out. println("handlerRemoved被调用"+ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
if(window.WebSocket){//支持webSocket编程
socket=new WebSocket("ws://localhost:8899/hello2");
//相当于channelReado, ev收到服务器端回送的消息
socket.onmessage = function (ev){
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" +ev.data;
}
//相当于连接开启(感知到连接开启)
socket.onopen = function (ev){
var rt = document.getElementById("responseText");
rt.value = "连接开启了.."
}
//相当于连接关闭(感知到连接关闭)
socket.onclose = function (ev){
var rt = document.getElementById("responseText");rt.value = rt.value + "\n"+"连接关闭了.."
}
}
//发送消息到服务器
function send(message) {
if (!window.socket) {//先判断socket是否创建好
return;
}
if (socket.readyState == WebSocket.OPEN) {
//通过socket发送消息
socket.send(message)
} else {
alert("连接没有开启");
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="width: 200px;height: 100px;"></textarea>
<input type="button" value="发送" onclick="send(this.form.message.value)">
<textarea id="responseText" style="width: 200px;height: 100px;"></textarea>
<input type="button" value="清空" onclick="document.getElementById('responseText').value='' ">
</form>
</body>
</html>
六、Google Protobuf
1编码和解码的基本介绍
1)编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图]
2) codec(编解码器)的组成部分有两个:decoder(解码器)和encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据
2 Netty本身的编码解码的机制和问题分析
1) Netty自身提供了一些codec(编解码器)
2) Netty提供的编码器
StringEncoder,对字符串数据进行编码
ObjectEncoder,对 Java对象进行编码
...
3) Netty提供的解码器
StringDecoder,对字符串数据进行解码
ObjectDecoder,对Java对象进行解码...
4) Netty本身自带的ObjectDecoder 和 ObjectEncoder可以用来实现 POJO对象或各种业务对象的编码和解码,底层使用的仍是Java 序列化技术,而Java序列化技术本身效率就不高,存在如下问题无法跨语言序列化后的体积太大,是二进制编码的5倍多。序列化性能太低
5)引出新的解决方案[Google 的 Protobuf]
7.3 Protobuf
1) Protobuf基本介绍和使用示意图
2) Protobuf是 Google 发布的开源项目,全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC[远程过程调用remote procedurecall ]数据交换格式。
目前很多公司http+json tcp+protobuf
3)参考文档 : https:lldevelopers.google.com/protocol-buffers/docs/proto语言指南
4) Protobuf是以 message 的方式来管理数据的.
5)支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如C++、C#、Java、python等)
6)高性能,高可靠性
7)使用protobuf编译器能自动生成代码,Protobuf是将类的定义使用.,proto 文件进行描述。说明,在 idea 中编
写.proto文件时,会自动提示是否下载.ptotot 编写插件.可以让语法高亮。
8)然后通过protoc.exe 编译器根据.proto自动生成.java文件
9)protobuf 使用示意图
4.案例
需求:
1)客户端可以发送一个 Student 对象到服务器(通过 Protobuf 编码)
2)服务端能接收Student对象,并显示信息(通过Protobuf解码)
1.导入所需依赖
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.6.1</version> </dependency>
2.编写Student.proto文件
syntax = "proto3"; //版本,必须在第一行,且上面不能有空行
option java_outer_classname = "studentPOJO";//生成的外部类名,同时也是文件名/ / protobuf使用message管理数据
message student {//会在studentPo30外部类生成一个内部类student,他是真正发送的PoJo对象
int32 id = 1; // student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}
proto文件消息类型对应的java类型
ProtoBuf 消息类型 | Java 类型 |
---|---|
double | double |
float | float |
int32 | int |
int64 | long |
uint32 | int |
uint64 | long |
sint32 | int |
sint64 | long |
fixed32 | int |
fixed64 | long |
sfixed32 | int |
sfixed64 | long |
bool | boolean |
string | String |
bytes | ByteString |
enum | 对应的 Java 枚举 |
message | 对应的 Java 类 |
3.编译刚刚写的proto文件
使用软件
得到编译后的java文件
我贴在下面编译好的内容,不可修改:
package NettyTest;// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: Student.proto
public final class studentPOJO {
private studentPOJO() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistryLite registry) {
}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
registerAllExtensions(
(com.google.protobuf.ExtensionRegistryLite) registry);
}
public interface studentOrBuilder extends
// @@protoc_insertion_point(interface_extends:student)
com.google.protobuf.MessageOrBuilder {
/**
* <pre>
* student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
* </pre>
*
* <code>int32 id = 1;</code>
*/
int getId();
/**
* <code>string name = 2;</code>
*/
String getName();
/**
* <code>string name = 2;</code>
*/
com.google.protobuf.ByteString
getNameBytes();
}
/**
* <pre>
*会在studentPo30外部类生成一个内部类student,他是真正发送的PoJo对象
* </pre>
*
* Protobuf type {@code student}
*/
public static final class student extends
com.google.protobuf.GeneratedMessageV3 implements
// @@protoc_insertion_point(message_implements:student)
studentOrBuilder {
private static final long serialVersionUID = 0L;
// Use student.newBuilder() to construct.
private student(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
super(builder);
}
private student() {
id_ = 0;
name_ = "";
}
@Override
public final com.google.protobuf.UnknownFieldSet
getUnknownFields() {
return this.unknownFields;
}
private student(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
this();
if (extensionRegistry == null) {
throw new NullPointerException();
}
int mutable_bitField0_ = 0;
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder();
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
case 8: {
id_ = input.readInt32();
break;
}
case 18: {
String s = input.readStringRequireUtf8();
name_ = s;
break;
}
default: {
if (!parseUnknownFieldProto3(
input, unknownFields, extensionRegistry, tag)) {
done = true;
}
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.setUnfinishedMessage(this);
} catch (java.io.IOException e) {
throw new com.google.protobuf.InvalidProtocolBufferException(
e).setUnfinishedMessage(this);
} finally {
this.unknownFields = unknownFields.build();
makeExtensionsImmutable();
}
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return studentPOJO.internal_static_student_descriptor;
}
@Override
protected FieldAccessorTable
internalGetFieldAccessorTable() {
return studentPOJO.internal_static_student_fieldAccessorTable
.ensureFieldAccessorsInitialized(
student.class, Builder.class);
}
public static final int ID_FIELD_NUMBER = 1;
private int id_;
/**
* <pre>
* student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
* </pre>
*
* <code>int32 id = 1;</code>
*/
public int getId() {
return id_;
}
public static final int NAME_FIELD_NUMBER = 2;
private volatile Object name_;
/**
* <code>string name = 2;</code>
*/
public String getName() {
Object ref = name_;
if (ref instanceof String) {
return (String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
String s = bs.toStringUtf8();
name_ = s;
return s;
}
}
/**
* <code>string name = 2;</code>
*/
public com.google.protobuf.ByteString
getNameBytes() {
Object ref = name_;
if (ref instanceof String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8(
(String) ref);
name_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
private byte memoizedIsInitialized = -1;
@Override
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized == 1) return true;
if (isInitialized == 0) return false;
memoizedIsInitialized = 1;
return true;
}
@Override
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
if (id_ != 0) {
output.writeInt32(1, id_);
}
if (!getNameBytes().isEmpty()) {
com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_);
}
unknownFields.writeTo(output);
}
@Override
public int getSerializedSize() {
int size = memoizedSize;
if (size != -1) return size;
size = 0;
if (id_ != 0) {
size += com.google.protobuf.CodedOutputStream
.computeInt32Size(1, id_);
}
if (!getNameBytes().isEmpty()) {
size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, name_);
}
size += unknownFields.getSerializedSize();
memoizedSize = size;
return size;
}
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof student)) {
return super.equals(obj);
}
student other = (student) obj;
boolean result = true;
result = result && (getId()
== other.getId());
result = result && getName()
.equals(other.getName());
result = result && unknownFields.equals(other.unknownFields);
return result;
}
@Override
public int hashCode() {
if (memoizedHashCode != 0) {
return memoizedHashCode;
}
int hash = 41;
hash = (19 * hash) + getDescriptor().hashCode();
hash = (37 * hash) + ID_FIELD_NUMBER;
hash = (53 * hash) + getId();
hash = (37 * hash) + NAME_FIELD_NUMBER;
hash = (53 * hash) + getName().hashCode();
hash = (29 * hash) + unknownFields.hashCode();
memoizedHashCode = hash;
return hash;
}
public static student parseFrom(
java.nio.ByteBuffer data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static student parseFrom(
java.nio.ByteBuffer data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static student parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static student parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static student parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static student parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static student parseFrom(java.io.InputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseWithIOException(PARSER, input);
}
public static student parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseWithIOException(PARSER, input, extensionRegistry);
}
public static student parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseDelimitedWithIOException(PARSER, input);
}
public static student parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseDelimitedWithIOException(PARSER, input, extensionRegistry);
}
public static student parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseWithIOException(PARSER, input);
}
public static student parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessageV3
.parseWithIOException(PARSER, input, extensionRegistry);
}
@Override
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder() {
return DEFAULT_INSTANCE.toBuilder();
}
public static Builder newBuilder(student prototype) {
return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
}
@Override
public Builder toBuilder() {
return this == DEFAULT_INSTANCE
? new Builder() : new Builder().mergeFrom(this);
}
@Override
protected Builder newBuilderForType(
BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
/**
* <pre>
*会在studentPo30外部类生成一个内部类student,他是真正发送的PoJo对象
* </pre>
*
* Protobuf type {@code student}
*/
public static final class Builder extends
com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
// @@protoc_insertion_point(builder_implements:student)
studentOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return studentPOJO.internal_static_student_descriptor;
}
@Override
protected FieldAccessorTable
internalGetFieldAccessorTable() {
return studentPOJO.internal_static_student_fieldAccessorTable
.ensureFieldAccessorsInitialized(
student.class, Builder.class);
}
// Construct using studentPOJO.student.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
private Builder(
BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
private void maybeForceBuilderInitialization() {
if (com.google.protobuf.GeneratedMessageV3
.alwaysUseFieldBuilders) {
}
}
@Override
public Builder clear() {
super.clear();
id_ = 0;
name_ = "";
return this;
}
@Override
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return studentPOJO.internal_static_student_descriptor;
}
@Override
public student getDefaultInstanceForType() {
return student.getDefaultInstance();
}
@Override
public student build() {
student result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
@Override
public student buildPartial() {
student result = new student(this);
result.id_ = id_;
result.name_ = name_;
onBuilt();
return result;
}
@Override
public Builder clone() {
return (Builder) super.clone();
}
@Override
public Builder setField(
com.google.protobuf.Descriptors.FieldDescriptor field,
Object value) {
return (Builder) super.setField(field, value);
}
@Override
public Builder clearField(
com.google.protobuf.Descriptors.FieldDescriptor field) {
return (Builder) super.clearField(field);
}
@Override
public Builder clearOneof(
com.google.protobuf.Descriptors.OneofDescriptor oneof) {
return (Builder) super.clearOneof(oneof);
}
@Override
public Builder setRepeatedField(
com.google.protobuf.Descriptors.FieldDescriptor field,
int index, Object value) {
return (Builder) super.setRepeatedField(field, index, value);
}
@Override
public Builder addRepeatedField(
com.google.protobuf.Descriptors.FieldDescriptor field,
Object value) {
return (Builder) super.addRepeatedField(field, value);
}
@Override
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof student) {
return mergeFrom((student)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(student other) {
if (other == student.getDefaultInstance()) return this;
if (other.getId() != 0) {
setId(other.getId());
}
if (!other.getName().isEmpty()) {
name_ = other.name_;
onChanged();
}
this.mergeUnknownFields(other.unknownFields);
onChanged();
return this;
}
@Override
public final boolean isInitialized() {
return true;
}
@Override
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
student parsedMessage = null;
try {
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
parsedMessage = (student) e.getUnfinishedMessage();
throw e.unwrapIOException();
} finally {
if (parsedMessage != null) {
mergeFrom(parsedMessage);
}
}
return this;
}
private int id_ ;
/**
* <pre>
* student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
* </pre>
*
* <code>int32 id = 1;</code>
*/
public int getId() {
return id_;
}
/**
* <pre>
* student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
* </pre>
*
* <code>int32 id = 1;</code>
*/
public Builder setId(int value) {
id_ = value;
onChanged();
return this;
}
/**
* <pre>
* student 类中有一个属性名字为 id类型为int32(protobuf类型) 1表示属性序号,不是值
* </pre>
*
* <code>int32 id = 1;</code>
*/
public Builder clearId() {
id_ = 0;
onChanged();
return this;
}
private Object name_ = "";
/**
* <code>string name = 2;</code>
*/
public String getName() {
Object ref = name_;
if (!(ref instanceof String)) {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
String s = bs.toStringUtf8();
name_ = s;
return s;
} else {
return (String) ref;
}
}
/**
* <code>string name = 2;</code>
*/
public com.google.protobuf.ByteString
getNameBytes() {
Object ref = name_;
if (ref instanceof String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8(
(String) ref);
name_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
/**
* <code>string name = 2;</code>
*/
public Builder setName(
String value) {
if (value == null) {
throw new NullPointerException();
}
name_ = value;
onChanged();
return this;
}
/**
* <code>string name = 2;</code>
*/
public Builder clearName() {
name_ = getDefaultInstance().getName();
onChanged();
return this;
}
/**
* <code>string name = 2;</code>
*/
public Builder setNameBytes(
com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
checkByteStringIsUtf8(value);
name_ = value;
onChanged();
return this;
}
@Override
public final Builder setUnknownFields(
final com.google.protobuf.UnknownFieldSet unknownFields) {
return super.setUnknownFieldsProto3(unknownFields);
}
@Override
public final Builder mergeUnknownFields(
final com.google.protobuf.UnknownFieldSet unknownFields) {
return super.mergeUnknownFields(unknownFields);
}
// @@protoc_insertion_point(builder_scope:student)
}
// @@protoc_insertion_point(class_scope:student)
private static final student DEFAULT_INSTANCE;
static {
DEFAULT_INSTANCE = new student();
}
public static student getDefaultInstance() {
return DEFAULT_INSTANCE;
}
private static final com.google.protobuf.Parser<student>
PARSER = new com.google.protobuf.AbstractParser<student>() {
@Override
public student parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return new student(input, extensionRegistry);
}
};
public static com.google.protobuf.Parser<student> parser() {
return PARSER;
}
@Override
public com.google.protobuf.Parser<student> getParserForType() {
return PARSER;
}
@Override
public student getDefaultInstanceForType() {
return DEFAULT_INSTANCE;
}
}
private static final com.google.protobuf.Descriptors.Descriptor
internal_static_student_descriptor;
private static final
com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
internal_static_student_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
}
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
String[] descriptorData = {
"\n\rStudent.proto\"#\n\007student\022\n\n\002id\030\001 \001(\005\022\014" +
"\n\004name\030\002 \001(\tB\rB\013studentPOJOb\006proto3"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
public com.google.protobuf.ExtensionRegistry assignDescriptors(
com.google.protobuf.Descriptors.FileDescriptor root) {
descriptor = root;
return null;
}
};
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
internal_static_student_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_student_fieldAccessorTable = new
com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
internal_static_student_descriptor,
new String[] { "Id", "Name", });
}
// @@protoc_insertion_point(outer_class_scope)
}
4.添加编码解码器
//服务器端解码器
pipeline.addLast("decoder",new ProtobufDecoder(studentPOJO.student.getDefaultInstance()));
//客户端编码器
pipeline.addLast("encode",new ProtobufEncoder());
5.服务器端我们的自定义处理器拿客户端传来的java对象:
public class NettyServerHandler extends SimpleChannelInboundHandler<studentPOJO.student> {
//读取数据
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx,studentPOJO.student student){
System.out.println(student.getName());
System.out.println(student.getId());
}
}
七、Netty 编解码器和 handler的调用机制
1.基本说明
1) ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现 ChannelInboundHandler 接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler 冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler 原理一样,只不过它是用来处理出站数据的
2) ChannelPipeline 提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline 中的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的
2.编码解码器
1)当Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象)﹔如果是出站消息,它会被编码成字节。
2) Netty提供一系列实用的编解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
3.解码器-ByteToMessageDecoder
1)关系继承图
2)由于不可能知道远程节点是否会一次性发送一个完整的信息,tcp有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.
4.Netty的handler 链的调用机制
Netty 中的 Handler 链(Pipeline)是一种事件处理机制,用于按顺序执行一系列的 ChannelHandler
。当一个事件发生时,它会在整个链上传播,每个 ChannelHandler
都有机会处理这个事件或者将其传递给下一个处理器。以下是 Netty Handler 链的调用机制:
-
Pipeline 构建: 在创建
Channel
时,Netty 会为每个Channel
创建一个对应的ChannelPipeline
。ChannelPipeline
就是 Handler 链,它是由多个ChannelHandler
组成的。 -
Handler 的添加: 在
ChannelPipeline
中,可以通过pipeline.addLast(handler)
方法添加ChannelHandler
。添加的顺序决定了事件传播的顺序。 -
事件传播: 当一个事件发生时,Netty 会从
ChannelPipeline
的头部开始,将事件传播给第一个ChannelHandler
。每个ChannelHandler
负责处理特定类型的事件。 -
Handler 处理: 当
ChannelHandler
的处理方法被调用时,它可以执行业务逻辑,修改事件的状态,或者传递事件给下一个处理器。在处理方法中,可以调用ChannelHandlerContext
对象的方法来触发下一个处理器的处理方法。
如:
如果按以下顺序添加处理器:
pipeline.addLast(handler1);
pipeline.addLast(handler2);
pipeline.addLast(handler3);
那么当事件传播时,首先会经过
handler1
,然后是handler2
,最后是handler3
。每个处理器都有机会处理事件或者将其传递给下一个处理器。
5.其他解码器
1) LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r'n.
作为分隔符来解析数据。
2) DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。
3) HttpObjectDecoder:一个HTTP数据的解码器
4)LengthFieldBasedFirameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。
6.Netty整合logf4
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
八、TCP 粘包和拆包及解决方案
1.基本介绍
-
粘包(TCP Packet Concatenation):
- 粘包指的是在数据传输过程中,多个小的数据包被组合成一个大的数据包一起发送到接收端。这可能导致接收端难以正确解析数据,因为数据的边界不清晰(面相流的通信是无消息保护边界的)。
-
拆包(TCP Packet Fragmentation):
- 拆包是粘包问题的相反,指的是一个大的数据包在传输过程中被分割成多个小的数据包发送到接收端。这也可能导致接收端无法正确还原原始数据。
3) 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图
4)示意图TCP粘包、拆包图解
对图说明:
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
1)服务端分两次读取到了两个独立的数据包,分别是D1和 D2,没有粘包和拆包
2)服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
3)服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
4)服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
2.TCP粘包拆包现象实例
代码:
server端处理器代码
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateEvent;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
//ChannelInboundHandlerAdapter:这是一个基础的入站处理器,你需要手动管理资源的释放。它的 channelRead 方法接收到的消息需要你自己负责释放
public class NettyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
static int onLinePeople;
static List<SocketChannel> socketChannelList=new ArrayList<>();
private int count;
//通道连接就绪触发连接就绪方法,配合连接关闭可以用于统计在线人数
@Override
public void channelActive(ChannelHandlerContext ctx){
onLinePeople+=1;
System.out.println("地址:"+ctx.channel().remoteAddress()+"用户上线");
System.out.println("当前在线人数为:"+onLinePeople);
SocketChannel channel =(SocketChannel) ctx.channel();
socketChannelList.add(channel);
}
@Override
public void channelInactive(ChannelHandlerContext ctx){
onLinePeople-=1;
System.out.println("地址:"+ctx.channel().remoteAddress()+"用户下线");
System.out.println("当前在线人数为:"+onLinePeople);
//移除下线的客户端通道
SocketChannel channel =(SocketChannel) ctx.channel();
for (SocketChannel socketChannel : socketChannelList) {
if(channel.equals(socketChannel)){
socketChannelList.remove(socketChannel);
}
}
}
//读取数据
/*
1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
2. Object msg:就是客户端发送的数据默认 Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx,ByteBuf msg){
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("客户端接收到消息="+ message);
System.out.println("客户端接收消息数量="+(++this.count));
//服务器会送一个数据给客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("我收到你的数据啦",Charset.forName("utf-8")));
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
//将evt向下转型IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress()+"--超时时间--"+eventType);System.out.println("服务器做相应处理..");
//如果发生空闲,我们关闭通道
//ctx.channel().close();
}
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
客户端处理器代码:
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
//与服务器通道就绪触发方法
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i=0;i<10;i++){
ByteBuf buf = Unpooled.copiedBuffer("hello server" + i, Charset.forName("utf-8"));
ctx.writeAndFlush(buf);
}
}
//通道有读取事件发生触发
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("客户端接收到消息="+ message);
System.out.println("客户端接收消息数量="+(++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
现象:
3.解决方案
1)使用自定义协议+编解码器来解决
2))关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP粘包、拆包。
4.案例
1)要求客户端发送5个Message 对象,客户端每次发送一个Message对象
2) 服务器端每次接收一个Message,分5次进行解码,每读取到一个Message ,会回复一个Message对象给客户端.
代码
服务器处理器
@Override
public void channelRead0(ChannelHandlerContext ctx,MessageProtocol msg) throws UnsupportedEncodingException {
//接收到数据并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务器接收到数据:");
System.out.println("长度:"+len);
System.out.println("内容:"+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到数量"+(++count));
//回复消息
String responseContent= UUID.randomUUID().toString();
int responseLen = responseContent.getBytes( "utf-8").length;
byte[]responseContent2 =responseContent.getBytes( "utf-8");
//构建一个协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(responseLen);
messageProtocol.setContent(responseContent2);
ctx.writeAndFlush(messageProtocol);
}
客户端处理器:
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i=0;i<5;i++){
String msg="今天天气冷,我想吃火锅"+i;
byte[] bytes = msg.getBytes(Charset.forName("utf-8"));
int length = bytes.length;
//创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(bytes);
ctx.writeAndFlush(messageProtocol);
}
}
自定义协议对象
package com.syc.one.test4;
public class MessageProtocol {
private int len;
private byte[] content;
public int getLen(){
return len;
}
public void setLen(int len){
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[ ] content){
this.content = content;
}
}
自定义编码器
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("encode方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
自定义解码器:
package com.syc.one.test4;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
public class MyMessageDecoder extends ReplayingDecoder<MessageProtocol> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
System.out.println("decode被调用");
int i = byteBuf.readInt();
byte[] content = new byte[i];
byteBuf.readBytes(content);
//封装成MessageProtocol对象,传给下一个处理器
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent(content);
messageProtocol.setLen(i);
list.add(messageProtocol);
}
}
然后在客户端和服务端各自的pipeline添加我们自定义的编码解码器
pipeline.addLast("decoder",new MyMessageDecoder());
pipeline.addLast("encoder",new MyMessageEncoder());