Netty快速学习1-基础知识回顾

Netty作为软件高级编程必学技术框架

传统BIO框架

  1. 网络编程的基本模型是 Client/Server 模型
  2. 两个进程间通信,服务端提供IP和端口,客户端发起连接请求,通过三次握手建立连接
  3. 双方连接建立成功,通过网络套接字(Socket)进行同步阻塞I/O (BIO)通信
  4. 一般情况下服务端的线程数和客户端请求线程数为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框架。

一般按照 操作的数据单元大小分为: 字节流字符流
按照 流的⻆⾊划分为 节点流处理流 下面表格中高 亮部分表示为节点流,所谓 “节点”就是具体的 写入或读取的 “文件” 或 “设备” 或者其他实际对象。

按照流的角色分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
抽象基类FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
特殊流DataInputStreamDataOutputStream

备注:
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.errsun最早的日志记录方式,不灵活也不可配置
Log4j(Log for Java)大佬:Ceki GülcüApache 建议Sun引入Log4j到java的标准库中,但是sun拒绝了
JUL(Java Util Logging)sun2002年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
Log4j2Apache2012年出品, 不是Log4j1.x升级,而是新项目Log4j2,不兼容Log4j1.x的

摘录最后总结图片,三个日志接口之间的相互转换桥接,相爱相杀。
在这里插入图片描述

选用指南:

  1. 使用日志接口的API而不是直接使用日志产品的API,一般选择 Slf4j
  2. 自己的项目只选择一款具体日志产品,一般选择 logback 或者 log4j2
  3. 把日志产品的依赖设置为Optional和runtime scope

题外扩展-https过程说明

这里涉及一些域名加密通讯概念,深入了解两点之间的数据传输过程:
https://blog.csdn.net/gzt19881123/article/details/119078215

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值