Netty作为软件高级编程必学技术框架
目录
传统BIO框架
- 网络编程的
基本模型是 Client/Server
模型 - 两个进程间通信,
服务端提供IP和端口
,客户端发起连接请求,通过三次握手
建立连接 - 双方连接建立成功,通过网络套接字(Socket)进行
同步阻塞I/O
(BIO)通信 - 一般情况下服务端的线程数和客户端请求线程数为
1 : 1
, 服务端可以创建一个专门线程池防止系统并发量太大而崩溃。
基础回顾-网络七层模型
复习一下,网际互联及OSI七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层,参考博客:https://blog.csdn.net/taotongning/article/details/81352985
每一层都是承上启下的作用,接受上层或下层的数据
发送数据端从应用层 ------》物理层
接受数据端从物理层 ------》应用层
OSI模型 ======= | 说明 | 举例 |
---|---|---|
应用层 | 为应用软件提供了很多服务 | FTP协议,SSH远程登录协议,SMTP/POP3邮件协议,HTTP协议等 |
表示层 | 格式化数据 | 数据的格式化,变成 JPG、GIF格式的数据,数据加密解密等,数据解压缩等 |
会话层 | 控制会话,建立管理终止应用程序会话 | 各类软件中 session的概念 |
传输层 | 提供可靠和尽力而为的端到端的数据传输 | TCP、UDP协议,3次握手建立连接,负责网络传输和会话建立。 |
网络层 | IP选址和路由选择 | 链路层 通过MAC地址进行数据通讯,必须同一个局域网,数据完全广播传输。IP地址的出现就是解决不同局域网之间的数据通讯,网关负责转发。 |
链路层 | 定义如何格式化二进制数据流,支持错误检测 | 以太网协议规定一组电信号称之为一个数据包,或者叫做一个“ 帧 ”。[head头部 发送接收的MAC地址] + [data数据部分 最短46字节,最长1500字节] 举例:交换机通过MAC地址转发数据,逻辑链路控制 |
物理层 | 物理传输、硬件、物理特性 | 二进制数据流0110…,光纤,网线,无线电波等,该层没有寻址的概念 |
基础回顾-TCP 报文
TCP报文属于传输层里面的概念,该层的由来:
网络层的IP地址
帮我们找到目标局域网
,链路层的MAC地址
帮我们找到局域网里面的主机
,传输层的端口号
帮我们找到具体的应用程序
。
我们电脑上使用的都是应用程序(微信,QQ,钉钉等),这些应用程序分别占用不同的端口,每一个端口对应一个应用程序。
关于TCP/IP相关知识参考:https://www.coonote.com/tcpip/tcp-ip-basic-knowledge.html
TCP的报文结构
- 序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
- 确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=刚接受到的报文里面的Seq + 1, 表示该报文送达接收端时,要求接收端检查 Ack是否为 你之前发的报文里面的Seq + 1
题外扩展-TCP三次握手与四次挥手
复习一下 TCP连接过程,借助网上不错的参考资料:
(1)先看下TCP 6大标志位
您可以看到在3次握手(SYN,ACK)和数据传输期间使用的2个标志。
与所有标志一样,值“1”表示该标志位在传输过程中起作用”
。
在此示例中,只有“SYN”标志被设置,表示这是新的TCP连接的第一个段。
除此之外,每个标志占一位,由于有6个标志,所以标志部分总共6位:
符号 | 说明 |
---|---|
SYN | 同步位 ,SYN =1表示建立连接请求 |
ACK | 确认位 ,ACK=1 表示要求接受方确认数据包是否成功接收 |
FIN | 终止位 ,FIN=1 表示通讯终止请求 |
RST | 重置位 ,RST=1 表示 TCP 连接中出现异常必须强制断开连接 |
URG | 紧急位 ,URG=1 表示这是紧急数据 |
PSH | 推送位 ,PSH=1 表示数据要尽快的交付给应用层 |
(2)3次握手建立连接
第一次握手:
Client发送 SYN=1,随机值seq=J 给Server,
Client进入SYN_SENT状态,等待Server确认
。
第二次握手:
Server收到数据包根据 SYN=1知道Client是要请求建立连接,
Server也发送SYN=1,ACK=1,ack=J+1,随机值 seq=K 给Client用于确认连接请求,
Server进入SYN_RCVD状态
。
第三次握手:
Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则
再次发送ACK=1,ack=K+1给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则
连接建立成功,Client和Server进入ESTABLISHED状态
,完成三次握手,
随后Client与Server之间可以开始传输数据了。
(3)4次挥手端口连接
第一次挥手:
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态
。
第二次挥手:
Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态
。
第三次挥手:
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态
。
第四次挥手:
Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态
,完成四次挥手。
如果有大量的连接,每次在连接、关闭时都要三次握手,四次挥手,会很明显会造成性能低下,因此,
HTTP有一种叫做keep connection的机制,它可以在传输数据后仍然保持连接,当客户端再次获取数据时,直接使用刚刚空闲下的连接而无需再次握手。
为什么要三次握手?
1次绝对不行,客户端想连就连,想发数据就发数据那还得了
2次可不可以? 能,但还是有问题,服务端接受到客户端请求就立马在服务端系统中创建连接资源,然后等待客户端应答,客户端的连接有可能是过期的历史请求连接。
4次 多余
- 三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)(序列号发给客户端,客户端判断过期了直接销毁过时的请求)
- 三次握⼿才可以同步双⽅的初始序列号
- 三次握⼿才可以避免资源浪费(就是延迟创建服务端所需资源)
为什么要四次握手?
一般疑问就是 第2、3步骤是否可以合并一步,反正都是服务端发起的?
客户端发起了FIN信号给服务端,如果服务端没有及时回复,在网络世界里面,客户端认为超时了会重试重发第一步的FIN信号。
BIO样例
服务端socket
package org.example.bio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* bio测试 服务端
*
* @author admin
*/
public class BioServer {
static Logger logger = LoggerFactory.getLogger(BioServer.class);
public static void main(String[] args) throws IOException {
// 客户端连接数
AtomicLong counter = new AtomicLong(0L);
// 服务端端口
int serverPort = 9999;
// 定义一个专门处理客户端请求的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 100, 20, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), (ThreadFactory) Thread::new);
// 创建服务端 socket
try (ServerSocket server = new ServerSocket(serverPort)) {
logger.info("socket编程,服务端启动,端口号: {}", serverPort);
while (true) {
// 等待一个客户端的请求,线程阻塞方式进行等待
Socket socket = server.accept();
logger.info("已经累计接受客户端请求数: {} 个", counter.addAndGet(1L));
executor.execute(new ClientAcceptHandler(socket));
}
} catch (Exception e) {
logger.error("服务端启动异常", e);
}
}
}
package org.example.bio;
import cn.hutool.core.date.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* bio测试 服务端接受请求后端线程处理
*
* @author admin
*/
public class ClientAcceptHandler implements Runnable {
static Logger logger = LoggerFactory.getLogger(ClientAcceptHandler.class);
private Socket socket;
public ClientAcceptHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String body;
while ((body = in.readLine()) != null && body.length() != 0) {
logger.info("客户端传递的信息 :{}", body);
out.println("来之服务端的响应回复信息: " + DateUtil.now() + "\n");
}
} catch (Exception e) {
logger.error("客户端处理异常", e);
} finally {
logger.info("客户端处理完毕 socket 被关闭 {}", socket.isClosed());
}
}
}
客户端
测试时候可以直接 CMD命令行下 通过 CURL命令或者 telnet命令进行测试:
服务端控制台打印信息如下:
一次CURL请求 其实是向服务端发起了4次TCP请求
用java实现一个客户端
package org.example.bio;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* bio测试 客户端
*
* @author admin
*/
public class BioClient {
static Logger logger = LoggerFactory.getLogger(BioClient.class);
public static void main(String[] args) {
// 服务端端口
int serverPort = 9999;
// 服务端地址
String serverHost = "localhost";
// 创建 socket
try (Socket socket = new Socket(serverHost, serverPort);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
// 发送信息给服务端
// 后面必须带上 \n ,否则会导致服务端in 流读取阻塞
out.println("client time " + DateUtil.now() + "\n");
// 读取服务端的响应信息
String serverbody = in.readLine();
if (StrUtil.isNotBlank(serverbody)) {
logger.info(serverbody);
}
} catch (Exception e) {
logger.error("客户启动异常", e);
}
}
}
Java里面的IO框架
网络通信必须掌握的javaIO框架! 上面的例子中包含了输入输出流程常见的操作, 这里记录总结一下 IO框架。
一般按照 操作的数据单元大小分为: 字节流
和 字符流
按照 流的⻆⾊划分为 节点流
和 处理流
下面表格中高 亮部分表示为节点流,所谓 “节点”就是具体的 写入或读取的 “文件” 或 “设备” 或者其他实际对象。
按照流的角色分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
备注:
System.in
作为 InputStream
类的对象实现标准输入,一般用法:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String msg = br.readLine();
System.out
作为 PrintStream
打印流类的的对象实现标准输出,可以调用它的print、println或write方法来输出各种类型的数据。
这里写一个简单的IO测试,文件copy操作
package org.example.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
/**
* @author admin
*/
public class StreamTest {
static Logger logger = LoggerFactory.getLogger(StreamTest.class);
public static void main(String[] args) throws Exception {
File file = new File("G:\\tmp.json");
File newFile = new File("G:\\newTmp.json");
if (newFile.exists() && newFile.delete()) {
logger.info("删除文件newTmp.json");
}
if (newFile.createNewFile()) {
logger.info("重新创建文件newTmp.json");
}
try (InputStream inputStream = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
OutputStream outputStream = new FileOutputStream(newFile);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream))) {
String text;
while ((text = reader.readLine()) != null) {
logger.info("读取tmp.json文本内容: {}", text);
bufferedWriter.write(text);
bufferedWriter.newLine();
logger.info("写入newTmp.json ...");
}
bufferedWriter.flush();
} catch (Exception e) {
logger.error("文件操作异常", e);
}
}
}
处理流 包装了 节点流,操作更加方便,最外面的包装流关闭了被包装的流全部将关闭。
Java里面的线程池
网络编程离不开多线程的探讨,掌握java里面的线程池是必备基础!
参考博客:
https://www.cnblogs.com/zincredible/p/10984459.html
https://www.jianshu.com/p/f030aa5d7a28
java里面的自带的线程池管理类 : ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 100, 20, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), (ThreadFactory) Thread::new);
// 启动一个线程
executor.execute(new MyRunnable());
ThreadPoolExecutor 的常用构造函数参数含义:
参数 | 说明 |
---|---|
int corePoolSize | 核心线程大小 |
int maximumPoolSize | 最大线程大小 |
long keepAliveTime | 超过corePoolSize的线程多久不活动被销毁时间 |
TimeUnit unit | 时间单位 |
BlockingQueue workQueue | 任务队列 |
ThreadFactory threadFactory | 线程池工厂 |
RejectedExecutionHandler handler | 拒绝策略 |
java 内置了常用的四种线程池 ,可由Executors
类来生成,它们底层全部由 ThreadPoolExecutor 生成
线程池 | 说明 |
---|---|
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务 |
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待 |
newScheduledThreadPool | 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行 |
newCachedThreadPool | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 |
网络IO模型
学习netty之前必须知道的几个IO模型,在弄懂IO模型之前,首先用通俗的示例来理清几个概念 同步 异步 阻塞 非阻塞
业务场景: 你去菜馆点一份菜吃,向菜馆老板报了菜名,老板说OK,马上去厨房做了
各种示例场景 | 对应的同/异步, 阻/非阻塞 |
---|---|
你一直站在厨房门口等 老板把菜做好,然后你自己把菜端到餐桌上 | 同步 阻塞 |
你报完菜名后就去餐桌上刷抖音去了 ,然后每隔1分钟问老板好了没 ,如果好了你 自己把菜端到餐桌上 | 同步非阻塞 |
你报完菜名后就去餐桌上坐着一动不动 ,然后菜好了,老板亲自把菜端到餐桌上 | 异步 阻塞 |
你报完菜名后就去餐桌上刷抖音去了 ,然后菜好了,老板亲自把菜端到餐桌上 | 异步 非阻塞 |
结论:
你是否亲自去端菜 ? 是=同步 否=异步
你是否可做其他事? 否=阻塞 是=非阻塞
有了上面概念,现在来看看一共五种IO的模型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO 和 异步IO
推荐博客:https://zhuanlan.zhihu.com/p/115912936
这里截取里面主要图片描述5中IO模型:
非阻塞IO
多路复用IO
信号驱动IO
异步IO
总结:
阻塞非阻塞说的是线程的状态
同步和异步说的是消息的通知机制
同步需要主动读写数据,异步是不需要主动读写数据
同步IO和异步IO是针对用户应用程序和内核的交互
题外扩展-Java日志框架
这里主要是nett测试用例经常用到打印日志,java的日志框架丰富多杂,如何选择一款合适的日志框架?需要了解日志框架的来龙去脉。
不错的微信软文: https://mp.weixin.qq.com/s/v6p6W0MPrtYmVSUwnbW8Dg
总结一下:
日志框架演进历史
日志框架 | 研发者 | 说 明 |
---|---|---|
System.out和System.err | sun | 最早的日志记录方式,不灵活也不可配置 |
Log4j(Log for Java) | 大佬:Ceki Gülcü | Apache 建议Sun引入Log4j到java的标准库中,但是sun拒绝了 |
JUL(Java Util Logging) | sun | 2002年2月Java1.4发布此日志产品 |
JCL(Jakarta Commons Logging) | Apache | 日志接口,提供了一个默认实现Simple Log |
Slf4j(Simple Logging Facade forJava) | 大佬:Ceki Gülcü | 2005年出品,也是一套新日志接口 |
Logback | 大佬:Ceki Gülcü | 2006出品, Logback完美实现了Slf4j |
Log4j2 | Apache | 2012年出品, 不是Log4j1.x升级,而是新项目Log4j2,不兼容Log4j1.x的 |
摘录最后总结图片,三个日志接口之间的相互转换桥接,相爱相杀。
选用指南:
- 使用日志接口的API而不是直接使用日志产品的API,一般选择 Slf4j
- 自己的项目只选择一款具体日志产品,一般选择 logback 或者 log4j2
- 把日志产品的依赖设置为Optional和runtime scope
题外扩展-https过程说明
这里涉及一些域名加密通讯概念,深入了解两点之间的数据传输过程:
https://blog.csdn.net/gzt19881123/article/details/119078215