【Go基础】Socket和WebSocket编程

news2024/11/17 12:26:18

文章目录

  • 一、Socket编程
    • 1. 网络通信过程
    • 2. TCP CS架构
      • 2.1 网络通信模型
      • 2.2 TCP协议解读
      • 2.3 Go TCP编程
    • 3. UDP CS架构
      • 3.1 UDP协议解读
      • 3.2 Go UDP编程
  • 二、WebSocket编程
    • 1. WebSocket协议解读
    • 2. WebSocket CS架构实现
    • 3. 聊于室实现

一、Socket编程

1. 网络通信过程

  • DMA:网卡和磁盘数据拷贝到内存流程比较固定,不涉及到运算操作,且非常耗时,在磁盘嵌入一个DMA芯片,完成上述拷贝工作,把CPU解脱出来,让CPU专注于运算
  • mmap:用户空间和内核空间映射同一块内存空间,从而达到省略将数据从内核缓冲区拷贝到用户空间的操作,用户空间通过映射直接操作内核缓冲区的数据

阻塞式网络I/O

在这里插入图片描述

非阻塞式网络I/O
在这里插入图片描述

多路复用网络I/O
在这里插入图片描述

socket把复杂的传输层协议封装成简单的接口,使应用层可以像读写文件一样进行网络数据的传输

在这里插入图片描述

socket通信过程
在这里插入图片描述

2. TCP CS架构

2.1 网络通信模型

OSI参考模型

在这里插入图片描述

TCP/IP模型

在这里插入图片描述

传输层数据大小的上限为MSS(Maximum Segment Size, 最大分段大小),网络接口层数据大小的上限为MTU(Maximum Transmit Unit, 最大传输单元)

2.2 TCP协议解读

MSS=MTU-ip首部-tcp首部,MTU视网络接口层的不同而不同,TCP在建立连接时通常需要协商双方的MSS值,应用层传输的数据大于MSS时需要分段

在这里插入图片描述

TCP首部
在这里插入图片描述

  • 前20个字节是固定的,后面还4N个可选字节(TCP选项)
  • 数据偏移:TCP数据部分距TCP开头的偏移量(一个偏移量是4个字节, TCP选项占4N个字节),亦即TCP首部的长度,所以TCP首部的最大长度是15*4=60个字节,即TCP选项最多有40个字节
  • 端口在tcp层指定,ip在IP层指定。端口占2个字节,则最大端口号为2^16-1=65535
  • 由于应用层的数据被分段了,为了在接收端对数据按顺序重组,需要为每段数据编个“序号”
  • TCP规定在连接建立后所有传送的报文段都必须把ACK设置为1

TCP建立连接
在这里插入图片描述

  • 第一次握手:TCP首部SYN=1,初始化一个序号=J。SYN报文段不能携带数据
  • 第二次握手:TCP首部SYN=1,ACK=1,确认号=J+1,初始化一个序号=K,此报文同样不携带数据
  • 第三次握手:SYN=1,ACK=1,序号=J+1,确认号=K+1。此次一般会携带真正需要传输的数据
  • 确认号:即希望下次对方发过来的序号值
  • SYN Flood 攻击始终不进行第三次握手,属于DDOS攻击的一种

TCP释放连接
在这里插入图片描述

  • TCP的连接是全双工(可以同时发送和接收)的连接,因此在关闭连接的时候,必须关闭传送和接收两个方向上的连接
  • 第一次挥手:FIN=1,序号=M
  • 第二次挥手:ACK=1,序号=M+1
  • 第三次挥手:FIN=1,序号=N
  • 第四次挥手:ACK=1,序号=N+1
  • 从TIME_WAIT进入CLOSED需要经过2个MSL(Maxinum Segment Lifetime),RFC793建议MSL=2分钟

2.3 Go TCP编程

  • 用三元给(ip地址,协议,端口号)唯一标示网络中的一个进程,如(172.122.121.111, tcp, 5656)
  • IPv4的地址位数为32位,分为4段,每段最大取值为255
  • IPv6的地址位数为128位,分为8段,各段用16进制表示,最大取值为ffff
  • 端口:0 ~ 1023 被熟知的应用程序占用(普通应用程序不可以使用),49152 ~ 65535客户端程序运行时动态选择使用
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)

net参数是“tcp4”、“tcp6”、“tcp”中的任意一个,分别表示TCP(IPv4-only),TCP(IPv6-only)或者TCP(IPv4,、IPv6的任意一个),addr表示域名或者IP地址,例如" www.qq.com:80" 或者"127.0.0.1:22"

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)

network参数是"tcp4"、“tcp6”、"tcp"中的任意一个,laddr表示本机地址,一般设置为nil,raddr表示远程的服务地址

func net.DialTimeout(network string, address string, timeout time.Duration) (net.Conn, error)

创建连接时设置超时时间

func (*net.conn) Write(b []byte) (int, error)

通过conn发送数据

func (net.Conn).Read(b []byte) (n int, err error)

从conn里读取数据,如果没有数据可读,会阻塞

func ioutil.ReadAll(r io.Reader) ([]byte, error)

从conn中读取所有内容,直到遇到error(比如连接关闭)或EOF

func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)

监听端口

func (l *TCPListener) Accept() (Conn, error)

阻塞,直到有客户端请求建立连接

func (*net.conn) Close() error

关闭连接

func (c *TCPConn) SetReadDeadline(t time.Time) error 
func (c *TCPConn) SetWriteDeadline(t time.Time) error

设置从一个tcp连接上读取和写入的超时时间

func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

当一个tcp连接上没有数据时,操作系统会间隔性地发送心跳包,如果长时间没有收到心跳包会认为连接已经断开

tcp_server.go

package main

import (
	"encoding/json"
	"fmt"
	"go-course/socket"
	"net"
	"strconv"
	"time"
)

type (
	Request struct {
		A int
		B int
	}
	Response struct {
		Sum int
	}
)

func handleRequest2(conn net.Conn) {
	conn.SetReadDeadline(time.Now().Add(30 * time.Second)) // 30秒后conn.Read会报出i/o timeout
	defer conn.Close()
	// 长连接,即连接建立后进行多轮的读写交互
	for {
		requestBytes := make([]byte, 256) // 初始化后byte数组每个元素都是0
		read_len, err := conn.Read(requestBytes)
		if err != nil {
			fmt.Printf("read from socket error: %s\n", err.Error())
			break // 到达deadline后,退出for循环,关闭连接,client再用这个连接读写会发生错误
		}
		fmt.Printf("receive request %s\n", string(requestBytes)) // []byte转string时,0后面的会自动被截掉

		var request socket.Request
		json.Unmarshal(requestBytes[:read_len], &request) // json反序列化时会把0都考虑在内,所以需要指定只读前read_len个字节
		response := socket.Response{Sum: request.A + request.B}

		responseBytes, _ := json.Marshal(response)
		_, err = conn.Write(responseBytes)
		socket.CheckError(err)
		fmt.Printf("write response %s\n", string(responseBytes))
	}
}

// 长连接
func main() {
	ip := "127.0.0.1" // ip换成0.0.0.0和空字符串试试
	port := 5656
	tcpAddr, err := net.ResolveTCPAddr("tcp4", ip+":"+strconv.Itoa(port))
	socket.CheckError(err)
	listener, err := net.ListenTCP("tcp4", tcpAddr)
	socket.CheckError(err)
	fmt.Println("waiting for client connection ......")
	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		fmt.Printf("establish connection to client %s\n", conn.RemoteAddr().String()) // 操作系统会随机给客户端分配一个49152~65535上的端口号
		go handleRequest2(conn)
	}
}

tcp_client.go

package main

import (
	"encoding/json"
	"fmt"
	"go-course/socket"
	"net"
	"strconv"
	"time"
)

// 长连接
func main() {
	ip := "127.0.0.1" // ip换成0.0.0.0和空字符串试试
	port := 5656
	conn, err := net.DialTimeout("tcp4", ip+":"+strconv.Itoa(port), 30*time.Minute)
	socket.CheckError(err)
	fmt.Printf("establish connection to server %s\n", conn.RemoteAddr().String())
	defer conn.Close()
	// 长连接,即连接建立后进行多轮的读写交互
	for {
		request := socket.Request{A: 7, B: 4}
		requestBytes, _ := json.Marshal(request)
		_, err = conn.Write(requestBytes)
		socket.CheckError(err)
		fmt.Printf("write request %s\n", string(requestBytes))
		responseBytes := make([]byte, 256) // 初始化后byte数组每个元素都是0
		read_len, err := conn.Read(responseBytes)
		socket.CheckError(err)
		var response socket.Response
		json.Unmarshal(responseBytes[:read_len], &response) // json反序列化时会把0都考虑在内,所以需要指定只读前read_len个字节
		fmt.Printf("receive response: %d\n", response.Sum)
		time.Sleep(1 * time.Second)
	}
}

3. UDP CS架构

3.1 UDP协议解读

在这里插入图片描述

  • UDP首部占8个字节,所以UDP报文长度最小是8B
  • 不需要建立连接,直接收发数据,效率很高
  • 面向报文,对应用层交下来的报文,既不合并也不拆分,直接加上边界交给IP层,TCP是面向字节流,TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送;如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去
  • 从机制上不保证顺序(在IP层要对数据分段),可能会丢包(检验和如果出差错就会把这个报文丢弃掉),在内网环境下分片乱序和数据丢包极少发生
  • 支持一对一、一对多、多对一和多对多的交互通信

3.2 Go UDP编程

func net.Dial(network string, address string) (net.Conn, error)

netwok指定为udp,建立udp连接(伪连接)

func net.DialTimeout(network string, address string, timeout time.Duration) (net.Conn, error)

netwok指定为udp,建立连接时指定超时

func net.ResolveUDPAddr(network string, address string) (*net.UDPAddr, error)

解析成udp地址

func net.ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error)

直接调用Listen就返回一个udp连接

func (*net.UDPConn).ReadFromUDP(b []byte) (int, *net.UDPAddr, error)

读数据,会返回remote的地址

func (*net.UDPConn).WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)

写数据,需要指定remote的地址
udp_server.go

package main

import (
	"encoding/json"
	"fmt"
	"go-course/socket"
	"net"
	"strconv"
	"time"
)

//长连接
func main() {
	ip := "127.0.0.1" // ip换成0.0.0.0和空字符串试试
	port := 5656
	udpAddr, err := net.ResolveUDPAddr("udp", ip+":"+strconv.Itoa(port))
	socket.CheckError(err)
	conn, err := net.ListenUDP("udp", udpAddr) // UDP不需要创建连接,所以不需要像TCP那样通过Accept()创建连接,这里的conn是个假连接
	socket.CheckError(err)
	conn.SetReadDeadline(time.Now().Add(30 * time.Second))
	defer conn.Close()
	for {
		requestBytes := make([]byte, 256) // 初始化后byte数组每个元素都是0
		read_len, remoteAddr, err := conn.ReadFromUDP(requestBytes) // 一个conn可以对应多个client,ReadFrom可以返回是哪个
		if err != nil {
			fmt.Printf("read from socket error: %s\n", err.Error())
			break // 到达deadline后,退出for循环,关闭连接。client再用这个连接读写会发生错误
		}
		fmt.Printf("receive request %s from %s\n", string(requestBytes), remoteAddr.String()) // []byte转string时,0后面的会自动被截掉

		var request socket.Request
		json.Unmarshal(requestBytes[:read_len], &request) // json反序列化时会把0都考虑在内,所以需要指定只读前read_len个字节
		response := socket.Response{Sum: request.A + request.B}

		responseBytes, _ := json.Marshal(response)
		_, err = conn.WriteToUDP(responseBytes, remoteAddr) // 由于UDP conn支持多对多通信,所以通信对方可能有多个EndPoint,通过WriteTo指定要写给哪个EndPoint
		socket.CheckError(err)
		fmt.Printf("write response %s to %s\n", string(responseBytes), remoteAddr.String())
	}
}

udp_client.go

package main

import (
	"encoding/json"
	"fmt"
	"go-course/socket"
	"net"
	"strconv"
	"sync"
	"time"
)

// 长连接
func main() {
	ip := "127.0.0.1" // ip换成0.0.0.0和空字符串试试
	port := 5656
	// 跟tcp_client的唯一区别就是这行代码
	conn, err := net.DialTimeout("udp", ip+":"+strconv.Itoa(port), 30*time.Minute) // 一个conn绑定一个本地端口
	socket.CheckError(err)
	defer conn.Close()
	const P = 10
	wg := sync.WaitGroup{}
	wg.Add(P)
	for i := 0; i < P; i++ {
		request := socket.Request{A: 7, B: 4}
		requestBytes, _ := json.Marshal(request)
		// 多协程,共用一个conn
		go func() {
			defer wg.Done()
			// 长连接,即连接建立后进行多轮的读写交互
			for {
				_, err = conn.Write(requestBytes)
				socket.CheckError(err)
				fmt.Printf("write request %s\n", string(requestBytes))
				responseBytes := make([]byte, 256) // 初始化后byte数组每个元素都是0
				read_len, err := conn.Read(responseBytes)
				socket.CheckError(err)
				var response socket.Response
				json.Unmarshal(responseBytes[:read_len], &response) // json反序列化时会把0都考虑在内,所以需要指定只读前read_len个字节
				fmt.Printf("receive response: %d\n", response.Sum)
				time.Sleep(1 * time.Second)
			}
		}()
	}
	wg.Wait()
}

由于UDP不需要建立连接,所以通过Dial()创建的是一个虚拟连接, Dial()总是会立即返回成功,即使对方还没有准备好,所以UDP可以先启client,再启server。由于是虚拟连接所以多个client可以共用一个conn,所以Server端往conn里写数据时需要指定写给哪个client,同理从conn里读数据会返回client的Address,即WriteToUDP (b []byte, addr *net.UDPAddr)和ReadFromUDP(b []byte) (int, *net.UDPAddr, error)。由于UDP是无连接和,对方关闭连接后,本方再在conn上调用Write和Read不会报错。

应用层的一条完整数据称为报文。TCP是面向字节流的,一次Read到的数据可能包含了多个报文,也可能只包含了半个报文,一条报文在什么地方结束需要通信双方事先约定好。UDP是面向报文的,一次Read只读一个报文,如果没有把一个报文读完,后面的内容会被丢弃掉,下次就读不到了。

二、WebSocket编程

1. WebSocket协议解读

在这里插入图片描述

websocket和http协议的关联:

  • 都是应用层协议,都基于tcp传输协议
  • 跟http有良好的兼容性,ws和http的默认端口都是80,wss和https的默认端口都是443
  • websocket在握手阶段采用http发送数据

websocket和http协议的差异:

  • http是半双工,而websocket通过多路复用实现了全双工
  • http只能由client主动发起数据请求,而websocket还可以由server主动向client推送数据。在需要及时刷新的场景中,http只能靠client高频地轮询,浪费严重
  • http是短连接(也可以实现长连接, HTTP1.1 的连接默认使用长连接),每次数据请求都得经过三次握手重新建立连接,而websocket是长连接
  • http长连接中每次请求都要带上header,而websocket在传输数据阶段不需要带header

WebSocket是HTML5下的产物,能更好的节省服务器资源和带宽,websocket应用场景举例:

  • html5多人游戏
  • 聊天室
  • 协同编辑
  • 基于实时位置的应用
  • 股票实时报价
  • 弹幕
  • 视频会议

websocket握手协议:
Request Header

Sec-Websocket-Version:13
Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Key:duR0pUQxNgBJsRQKj2Jxsw==

Response Header

Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Accept:a1y2oy1zvgHsVyHMx+hZ1AYrEHI=
  • Upgrade:websocket和Connection:Upgrade指明使用WebSocket协议
  • Sec-WebSocket-Version 指定Websocket协议版本
  • Sec-WebSocket-Key是一个Base64 encode的值,是浏览器随机生成的
  • 服务端收到Sec-WebSocket-Key后拼接上一个固定的GUID,进行一次SHA-1摘要,再转成Base64编码,得到Sec-WebSocket-Accept返回给客户端;客户端对本地的Sec-WebSocket-Key执行同样的操作跟服务端返回的结果进行对比,如果不一致会返回错误关闭连接。如此操作是为了把websocket header跟http header区分开

2. WebSocket CS架构实现

首先需要安装gorilla的websocket包

go get github.com/gorilla/websocket

将http升级到WebSocket协议

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, error)

客户端发起握手,请求建立连接

func (*websocket.Dialer) Dial(urlStr string, requestHeader http.Header) (*websocket.Conn, *http.Response, error)

基于connection进行read和write

ws_server.go

package main

import (
	"fmt"
	"go-course/socket"
	"net"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/websocket"
)

type WsServer struct {
	listener net.Listener
	addr     string
	upgrade  *websocket.Upgrader
}

func NewWsServer(port int) *WsServer {
	ws := new(WsServer)
	ws.addr = "0.0.0.0:" + strconv.Itoa(port)
	ws.upgrade = &websocket.Upgrader{
		HandshakeTimeout: 5 * time.Second, // 握手超时时间
		ReadBufferSize:   2048,            // 读缓冲大小
		WriteBufferSize:  1024,            // 写缓冲大小
		// 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造,如果Origin请求头存在且原始主机不等于请求主机头,则返回false
		CheckOrigin: func(r *http.Request) bool {
			fmt.Printf("request url %s\n", r.URL)
			fmt.Println("handshake request header")
			for key, values := range r.Header {
				fmt.Printf("%s:%s\n", key, values[0])
			}
			return true
		},
		// http错误响应函数
		Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {},
	}
	return ws
}

// httpHandler必须实现ServeHTTP接口
func (ws *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/add" {
		fmt.Println("path error")
		http.Error(w, "请求的路径不存在", 222) // 把出错的话术写到ResponseWriter里
		return
	}
	conn, err := ws.upgrade.Upgrade(w, r, nil) // 将http协议升级到websocket协议
	if err != nil {
		fmt.Printf("upgrade http to websocket error: %v\n", err)
		return
	}
	fmt.Printf("establish conection to client %s\n", conn.RemoteAddr().String())
	go ws.handleConnection(conn)
}

// 处理连接里发来的请求数据
func (ws *WsServer) handleConnection(conn *websocket.Conn) {
	defer func() {
		conn.Close()
	}()
	// 长连接
	for {
		conn.SetReadDeadline(time.Now().Add(20 * time.Second))
		var request socket.Request
		if err := conn.ReadJSON(&request); err != nil {
			// 判断是不是超时
			if netError, ok := err.(net.Error); ok { // 如果ok==true,说明类型断言成功
				if netError.Timeout() {
					fmt.Printf("read message timeout, remote %s\n", conn.RemoteAddr().String())
					return
				}
			}
			// 忽略websocket.CloseGoingAway/websocket.CloseNormalClosure这2种closeErr,如果是其他closeErr就打一条错误日志
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
				fmt.Printf("read message from %s error %v\n", conn.RemoteAddr().String(), err)
			}
			return // 只要ReadMessage发生错误,就关闭这条连接
		} else {
			response := socket.Response{Sum: request.A + request.B}
			if err = conn.WriteJSON(&response); err != nil {
				fmt.Printf("write response failed: %v", err)
			} else {
				fmt.Printf("write response %d\n", response.Sum)
			}
		}
	}
}

func (ws *WsServer) Start() (err error) {
	ws.listener, err = net.Listen("tcp", ws.addr) // http和websocket都是建立在tcp之上的
	if err != nil {
		fmt.Printf("listen error:%s\n", err)
		return
	}
	err = http.Serve(ws.listener, ws) // 开始对外提供http服务。可以接收很多连接请求,其他一个连接处理出错了,也不会影响其他连接
	if err != nil {
		fmt.Printf("http server error: %v\n", err)
		return
	}

	// if err:=http.ListenAndServe(ws.addr, ws);err!=nil{ // Listen和Serve两步合成一步
	// 	fmt.Printf("http server error: %v\n", err)
	// 	return
	// }
	return nil
}

func main() {
	ws := NewWsServer(5657)
	ws.Start()
}

ws_client.go

package main

import (
	"encoding/json"
	"fmt"
	"go-course/socket"
	"io/ioutil"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

func main() {
	dialer := &websocket.Dialer{}
	header := http.Header{
		"Cookie": []string{"name=zcy"},
	}
	conn, resp, err := dialer.Dial("ws://localhost:5657/add", header) // Dial:握手阶段,会发送一条http请求,请求一个不存在的路径试试看
	defer resp.Body.Close()
	if err != nil {
		fmt.Printf("dial server error:%v\n", err)
		fmt.Println(resp.StatusCode)
		msg, _ := ioutil.ReadAll(resp.Body)
		fmt.Println(string(msg))
		return
	}
	fmt.Println("handshake response header")
	for key, values := range resp.Header {
		fmt.Printf("%s:%s\n", key, values[0])
	}
	// time.Sleep(5 * time.Second)
	defer conn.Close()
	for i := 0; i < 10; i++ {
		request := socket.Request{A: 7, B: 4}
		requestBytes, _ := json.Marshal(request)
		err = conn.WriteJSON(request) // websocket.Conn直接提供发json序列化和反序列化方法
		socket.CheckError(err)
		fmt.Printf("write request %s\n", string(requestBytes))
		var response socket.Response
		err = conn.ReadJSON(&response)
		socket.CheckError(err)
		fmt.Printf("receive response: %d\n", response.Sum)
		time.Sleep(1 * time.Second)
	}
	time.Sleep(30 * time.Second)
}

websocket发送的消息类型有5种:TextMessag,BinaryMessage, CloseMessag,PingMessage,PongMessage
TextMessag和BinaryMessage分别表示发送文本消息和二进制消息
CloseMessage关闭帧,接收方收到这个消息就关闭连接
PingMessage和PongMessage是保持心跳的帧,发送方接收方是PingMessage,接收方发送方是PongMessage,目前浏览器没有相关api发送ping给服务器,只能由服务器发ping给浏览器,浏览器返回pong消息

3. 聊于室实现

gorilla的websocket项目中有一个聊天室的demo,此处讲一下它的设计思路
基于原代码进行了简化和修改,并加上中文注释,总体架构如下图所示
在这里插入图片描述

Hub

  • Hub持有每一个Client的指针,broadcast管道里有数据时把它写入每一个Client的send管道中
  • 注销Client时关闭Client的send管道

Client

  • 前端(browser)请求建立websocket连接时,为这条websocket连接专门启一个协程,创建一个client
  • client把前端发过来的数据写入hub的broadcast管道
  • client把自身send管道里的数据写给前端
  • client跟前端的连接断开时请求从hub那儿注销自己

Front

  • 当打开浏览器页面时,前端会请求建立websocket连接
  • 关闭浏览器页面时会主动关闭websocket连接

存活监测

  • 当hub发现client的send管道写不进数据时,把client注销掉
  • client给websocket连接设置一个读超时,并周期性地给前端发ping消息,如果没有收到pong消息则下一次的conn.read()会报出超时错误,此时client关闭websocket连接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/353749.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch - Configuring security in Elasticsearch 开启用户名和密码访问

文章目录概述实操Step 1 验证当前版本是否支持安全功能Step 2 打开安全设置Step 3 配置节点间通讯传输的安全性创建证书颁发机构为Elasticsearch集群中的节点生成证书Step 4 修改 elasticsearch.yml配置设置 用户名和密码概述 ES版本&#xff1a; 7.6 官方指导手册&#xff1…

Spring MVC之WebApplicationContext 容器的初始化

简介因为 spring-mvc项目&#xff0c;是 spring-framework的子项目&#xff0c;所以需要拉取 spring-framework整个工程&#xff0c;包含 Spring 所有的子项目前期准备工作源码拉取从 Spring 的 Git 仓库 Fork 项目到自己的 Git 仓库&#xff0c;方便我们在阅读源码的过程中&am…

3年软件测试工作经验裸辞,有点后悔了...

2019年毕业&#xff0c;现在有3年的软件测试工作经验&#xff0c;刚毕业前半年在一家知名上市公司&#xff0c;后面则进入一家传统行业公司待到现在2年半。 由于看不到技术成长以及其他原因&#xff0c;上上周辞职了&#xff0c;目前交接中&#xff0c;下个月中旬就得离开了&a…

基于国产龙芯 CPU 的气井工业网关研究与设计(一)

当前&#xff0c;我国气田的自动化控制程度还未完全普及&#xff0c;并且与世界已普及的气井站的自 动化程度也存在一定的差距。而在天然气资源相对丰富的国家&#xff0c;开采过程中设备研发资 金投入较大&#xff0c;研发周期较长&#xff0c;更新了一代又一代的自动化开采系…

看过来,u盘删除的文件还能找回吗?两种方法,

u盘删除的文件还能找回吗&#xff1f;u盘&#xff0c;全称USB闪存驱动器&#xff0c;它不需物理驱动器&#xff0c;即插即用&#xff0c;且其存储容量远超过软盘&#xff0c;方便携带使用。u盘作为我们常用的存储设备&#xff0c;也是有自己不同功能和划分。以下例举了几种&…

ArcGIS API for JavaScript 4.15系列(8)——Dojo中类的定义

1、前言 JavaScript本质上是基于原型继承的一种编程语言&#xff0c;在ES6标准出现以前&#xff0c;JavaScript定义类的方式往往让人很难理解。而Dojo则很好地解决了这个问题。开发者可以通过dojo/_base/declare模块定义类&#xff0c;也可以通过define引用各个类模块。本文就…

Swift基础语法 - 可选项

可选项&#xff08;Optional&#xff09; 可选项&#xff0c;一般也叫可选类型&#xff0c;它允许将值设置为 nil 在类型名称后面加个问号 ? 来定义一个可选项 var name: String? "CSDN" name nilvar age: Int? //默认就是nil age 30 age nilvar array [2,…

提升电脑运行速度,看这里就够了!

电脑是我们经常使用的工具之一&#xff0c;但是它却很容易出现问题&#xff0c;比如运行速度过慢&#xff0c;那么要如何提升电脑运行速度呢&#xff1f;方法1. 通过系统配置设置启动项1. 按下组合键“WinR”打开“运行”&#xff0c;在运行中输入“msconfig”然后按“回车”。…

LVGL-基于Windows系统Visual Studio模拟器搭建

LVGL-基于Windows系统Visual Studio模拟器搭建简述下载安装Visual Studio下载LVGL源码运行效果简述 LVGL是一个轻量级多功能图形库 丰富且强大的模块化图形组件&#xff1a;按钮 (buttons)、图表 (charts)、列表 (lists)、滑动条 (sliders)、图片 (images) 等高级的图形引擎&…

UUID简介以及java代码获取UUID示例

什么是UUIDUUID 是指&#xff08;UniversallyUnique Identifier&#xff09;通用唯一识别码&#xff0c;128位。RFC 4122描述了具体的规范实现。现实问题我们开发的时候&#xff0c;数据库表总会有一个主键&#xff0c;以前我们可能会使用自增的数字作为主键。这样做去确实查询…

PyTorch使用Visdom绘制训练过程曲线

最近在训练网络&#xff0c;网络的损失和准确率都是在终端输出的&#xff0c;很不直观&#xff0c;也看不出变化&#xff0c;就想着有没有一种工具可以实现实时的绘制模型的训练过程&#xff0c;然后就搜到了Visdom&#xff0c;发现这是个好东西啊&#xff0c;完全满足了我的需…

【Kafka】二.Kafka消息发布/消费流程

Kafka 通过对消费方进行分组管理来支持消息一写多读。 我画的图&#xff1a;工具&#xff08;processon在线画图&#xff09; 这个 Topic 分为 4 个 Partition&#xff0c;就是图中的 P1到 P4&#xff0c;上部的生产方根据规则选择一个 Partition 进行写入&#xff0c;默认规则…

算法练习-链表(一)

算法练习-链表&#xff08;一&#xff09; 文章目录算法练习-链表&#xff08;一&#xff09;解题技巧1. 实现链表1.1 节点的定义1.2 链表的遍历1.3 节点的查找1.4节点的插入1.4.1 链头插入1.4.2 链尾插入1.4.3 在给定节点之后插入1.5 删除节点1.5.1 删除给定节点之后的节点、1…

如何快速完成园区数据的可视化分析?

对于园区运营方来说&#xff0c;如果没有专业针对性的管理方案以及管理系统辅助的话&#xff0c;实现园区可视化管理的难度非常大&#xff0c;而且操作成本会很高。但如果园区运营方选择引进快鲸智慧楼宇推出的园区数据孪生可视化管理系统的话就会简单很多。 快鲸智慧楼宇数据孪…

python项目使用pipenv管理环境,如何使用pycharm调试模式

写在开始 了解pipenv的朋友们应该知道&#xff0c;使用pipenv管理项目环境后&#xff0c;仅pipenv虚拟环境中安装有项目的依赖包&#xff0c;测试机本身并没有安装这个依赖包&#xff0c;这就是pipenv使用的意义。但是&#xff0c;如果想要进入调试模式呢&#xff1f;我们要如…

Coqui TTS docker 使用记录

前言 之前介绍过 Coqui TTS 的安装&#xff0c;不过那个环境被我玩挂掉了…… 这次记录一下 docker 版本的使用。 参考网址&#xff1a;Docker images - TTS 0.11.1 documentation 正文 首先按照官网指示先把镜像 pull 下来。&#xff08;后记&#xff1a;确保 GPU driver…

重生之我是赏金猎人-番外篇-记一次层层突破的攻防演练

0x00 前言 本文简单记述了一下本人在某攻防演练过程中一次层层突破的有趣经历 技术性一般&#xff0c;但是层层突破属实艰难&#xff0c;并用到了比较多的思路&#xff0c;还望各位大佬多多指教 0x01 SSO账号获取 由于目标是某大学&#xff0c;对外开放的服务基本上都是一些…

CSS 预处理工具 Less 的介绍及使用 步骤

文章目录Less是什么Less的使用方法Less 中的注释Less 中的变量Less 中的嵌套Less 中的混合&#xff08;Mixin&#xff09;Less 中的运算Less 中的转译Less 中的作用域Less 中的导入Less实用实例文字超出省略文字垂直居中定位上下左右居中Less是什么 Less 是一门 CSS 预处理语言…

spring的事务控制

1.调用这个方法的对象是否是spring的代理对象&#xff08;$CGLIB结尾的&#xff09; 2.这个方法是否是加了Transactional注释 都符合才可以被事物控制 如果调用方法的对象没有被事物控制&#xff0c;那么被调用的方法即便是加了Transactional也是没用的 事务失效情况&#xf…

基于MATLAB计算MIMO信道容量(附完整代码与分析)

目录 一.介绍 二. 代码 三. 运行结果及分析 3.1 MIMO信道容量&#xff1a;固定发射天线数为4 3.2 MIMO信道容量&#xff1a;固定接收天线数为4 3.3 AWGN信道与瑞利信道容量 四. 总结 一.介绍 本文章将在MATLAB环境中分析MIMO信道容量&#xff0c;AWGN信道容量&#xf…