Golang网络聊天室案例

news2024/11/26 8:55:07

1.聊天室设计分析

一. 概览

实现 个网络聊天室(群)
功能分析:

  1. 上线下线
  2. 聊天,其他人,自己都可以看到聊天消息
  3. 查询当前聊天室用户名字 who
  4. 可以修改自己名字 rename | Duke
  5. 超时踢出

技术点分析:
1 . sock tcp 编程
2 . map结构 (存储当前用户,map遍历,map删除)
3 . go程,channel
4 . select(超时退出,主动退出)
5 . timer定时器

二、实现基础

第一阶段:
tcp socket,建立多个连接

package main

import (
	"fmt"
	"net"
)

func main(){
	// 创建服务器
	listener,err := net.Listen("tcp",":8088")
	if err != nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("==>主go程监听中......")
		// 监听
		conn,err := listener.Accept()
		if err != nil{
			fmt.Println("listener.Accept err:",err)
			return
		}

		// 建立连接
		fmt.Println("建立连接成功!")
		// 启动处理业务的go程
		go handler(conn)

	}
}

// 处理具体业务
func handler(conn net.Conn){
	for{
		fmt.Println("启动业务...")
		// TODO // 代表这里以后再具体实现
		buf := make([]byte,1024)
		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
	}
	
}

go run chatroom.go
启动nc
在这里插入图片描述
nc下载地址

2、定义User/map结构

type User struct {
	// 名字
	name string 
	// 唯一 的 id
	id string
	// 管道
	msg chan string

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

在Handler中调用

// 处理具体业务
func handler(conn net.Conn){
	for{
		fmt.Println("启动业务...")
//
		// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
		clientAddr := conn.RemoteAddr().String()
		fmt.Println("clientAddr:",clientAddr)
		// 创建user
		newUser := User{
			id:clientAddr,// id 我们不会修改,这个作为map中的key
			name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
			msg:make(chan string), // 注意需要make空间,否则无法写入数据
		}
		// 添加user到map结构
		allUsers[newUser.id] = newUser
/
		buf := make([]byte,1024)
		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
	}
	

3.定义message管道

在这里插入图片描述
创建监听广播go程函数

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
	fmt.Println("广播go程启动成功...")
	// 1. 从message中读取数据
	info := <-message 
	// 2. 将数据写入到每一个用户的msg管道中
	for _,user := range allUsers{
		user.msg <- info 
	}
}

启动,全局唯一
在这里插入图片描述
写入上线数据
在这里插入图片描述
当前整体源码

package main

import (
	"fmt"
	"net"
)

type User struct {
	// 名字
	name string 
	// 唯一 的 id
	id string
	// 管道
	msg chan string

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

// 定义一个message全局通道,用于接收任何人发送过来消息
var message = make(chan string,10)

func main(){
	// 创建服务器
	listener,err := net.Listen("tcp",":8087")
	if err != nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	// 启动全局唯一的go程,负责监听message通道,写给所有的用户
	go broadcast()

	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("==>主go程监听中......")
		// 监听
		conn,err := listener.Accept()
		if err != nil{
			fmt.Println("listener.Accept err:",err)
			return
		}

		// 建立连接
		fmt.Println("建立连接成功!")
		// 启动处理业务的go程
		go handler(conn)

	}
}

// 处理具体业务
func handler(conn net.Conn){
	for{
		fmt.Println("启动业务...")
		// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
		clientAddr := conn.RemoteAddr().String()
		fmt.Println("clientAddr:",clientAddr)
		// 创建user
		newUser := User{
			id:clientAddr,// id 我们不会修改,这个作为map中的key
			name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
			msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
		}
		// 添加user到map结构
		allUsers[newUser.id] = newUser

		// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
		loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
		message <- loginInfo

		buf := make([]byte,1024)
		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
	}
	
}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
	fmt.Println("广播go程启动成功...")
	defer fmt.Println("broadcast 程序退出!")

	for {
		// 1. 从message中读取数据
		fmt.Println("broadcast监听message中...")
		info := <-message 
		// 2. 将数据写入到每一个用户的msg管道中
		for _,user := range allUsers{
			// 如果msg是非缓冲,那么会在这里阻塞
			user.msg <- info 
		}
	}
}

4.user监听通道go程

每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
	fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
	for data := range user.msg{
		fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

		// Write(b []byte)(n int,err error)
		_,_ = conn.Write([]byte(data))
	}
}

在这里插入图片描述
当前代码整体为

package main

import (
	"fmt"
	"net"
)

type User struct {
	// 名字
	name string 
	// 唯一 的 id
	id string
	// 管道
	msg chan string

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

// 定义一个message全局通道,用于接收任何人发送过来消息
var message = make(chan string,10)

func main(){
	// 创建服务器
	listener,err := net.Listen("tcp",":8087")
	if err != nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	// 启动全局唯一的go程,负责监听message通道,写给所有的用户
	go broadcast()

	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("==>主go程监听中......")
		// 监听
		conn,err := listener.Accept()
		if err != nil{
			fmt.Println("listener.Accept err:",err)
			return
		}

		// 建立连接
		fmt.Println("建立连接成功!")
		// 启动处理业务的go程
		go handler(conn)

	}
}

// 处理具体业务
func handler(conn net.Conn){

	fmt.Println("启动业务...")
	// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
	clientAddr := conn.RemoteAddr().String()
	fmt.Println("clientAddr:",clientAddr)
	// 创建user
	newUser := User{
		id:clientAddr,// id 我们不会修改,这个作为map中的key
		name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
		msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
	}
	// 添加user到map结构
	allUsers[newUser.id] = newUser

	// 启动go程,负责将msg的信息返回给客户端
	go writeBackToClient(&newUser,conn)

	// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
	loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
	message <- loginInfo

	for{
		// 具体业务逻辑

		buf := make([]byte,1024)
		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
	}
	
}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
	fmt.Println("广播go程启动成功...")
	defer fmt.Println("broadcast 程序退出!")

	for {
		// 1. 从message中读取数据
		fmt.Println("broadcast监听message中...")
		info := <-message 
		// 2. 将数据写入到每一个用户的msg管道中
		for _,user := range allUsers{
			// 如果msg是非缓冲,那么会在这里阻塞
			user.msg <- info 
		}
	}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
	fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
	for data := range user.msg{
		fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

		// Write(b []byte)(n int,err error)
		_,_ = conn.Write([]byte(data))
	}
}

三、增加功能

  1. 查询用户
    查询命令:who==>将当前所有的登录的用户,展示出来,id,name,返回给当前用户
package main

import (
	"fmt"
	"net"
	"strings"
)

type User struct {
	// 名字
	name string 
	// 唯一 的 id
	id string
	// 管道
	msg chan string

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

// 定义一个message全局通道,用于接收任何人发送过来消息
var message = make(chan string,10)

func main(){
	// 创建服务器
	listener,err := net.Listen("tcp",":8087")
	if err != nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	// 启动全局唯一的go程,负责监听message通道,写给所有的用户
	go broadcast()

	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("==>主go程监听中......")
		// 监听
		conn,err := listener.Accept()
		if err != nil{
			fmt.Println("listener.Accept err:",err)
			return
		}

		// 建立连接
		fmt.Println("建立连接成功!")
		// 启动处理业务的go程
		go handler(conn)

	}
}

// 处理具体业务
func handler(conn net.Conn){

	fmt.Println("启动业务...")
	// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
	clientAddr := conn.RemoteAddr().String()
	fmt.Println("clientAddr:",clientAddr)
	// 创建user
	newUser := User{
		id:clientAddr,// id 我们不会修改,这个作为map中的key
		name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
		msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
	}
	// 添加user到map结构
	allUsers[newUser.id] = newUser

	// 启动go程,负责将msg的信息返回给客户端
	go writeBackToClient(&newUser,conn)

	// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
	loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
	message <- loginInfo

	for{
		// 具体业务逻辑

		buf := make([]byte,1024)
		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
		// -------------业务逻辑处理  开始-------------	
		// 1.查询当前所有的用户 who
		// a. 先判断接收的数据是不是who  ==》 长度&&字符串
		userInput := string(buf[:cnt-1])  // 这是用户输入的数据,最后一个是回车,我们去掉他
		if len(userInput) == 3 && userInput == "who"{
			// b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端
			fmt.Println("用户即将查询所有用户信息!")
			// 这个切片包含所有的用户信息
			var userInfos []string
			for _,user := range allUsers{
				userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
				userInfos = append(userInfos,userInfo)
			}

			// 最终写入到通道中,一定是一个字符串
			r := strings.Join(userInfos,"\n")

			// 将数据返回到客户端
			newUser.msg <- r
		}else{
			// 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发
			message <- userInput
		}

		// -------------业务逻辑处理  结束-------------	

	}
	
}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
	fmt.Println("广播go程启动成功...")
	defer fmt.Println("broadcast 程序退出!")

	for {
		// 1. 从message中读取数据
		fmt.Println("broadcast监听message中...")
		info := <-message 
		// 2. 将数据写入到每一个用户的msg管道中
		for _,user := range allUsers{
			// 如果msg是非缓冲,那么会在这里阻塞
			user.msg <- info 
		}
	}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
	fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
	for data := range user.msg{
		fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

		// Write(b []byte)(n int,err error)
		_,_ = conn.Write([]byte(data))
	}
}
  1. 重命名
    规则:rename|Duke
    获取数据判断长度7,判断字符是rename
    使用|进行分割,获取|后面的部分,作为名字
    更新用户名字newUser.name = Duke
    通知客户端,更新成功

  2. 主动退出
    每个用户都有自己的watch go程,仅负责监听退出信号

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit <-chan bool){
	fmt.Println("启动监听信号退出的go程...")
	defer fmt.Println("watch go程退出!")
	for{
		select{
			case <-isQuit:
				logoutInfo := fmt.Sprintf("%s exit already!",user.name)
				fmt.Println("删除当前用户:",user.name)
				delete(allUsers,user.id)
				message<-logoutInfo
				conn.Close()
				return
		}
	}
}
在handler中启动go watch,同时传入相应信息:
	// 定义一个退出信号,用来监听client退出
	var isQuit = make(chan bool)

	// 启动go程,负责监听退出信号
	go watch(&newUser,conn,isQuit)

在read之后,通过cnt判断用户退出,向isQuit写入信号:

在这里插入图片描述

测试截图
在这里插入图片描述

  1. 超时退出
    使用定时器来进行超时管理
    如果60s没有发送任何数据,那么直接将这个链接关闭
<-time.After(60*time.second)

更新watch函数

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){
	fmt.Println("启动监听信号退出的go程...")
	defer fmt.Println("watch go程退出!")
	for{
		select{
			case <-isQuit:
				logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
				fmt.Println("删除当前用户:",user.name)
				delete(allUsers,user.id)
				message<-logoutInfo

				conn.Close()
				return
			case <-time.After(10*time.Second):
				logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name)
				fmt.Println("删除当前用户:",user.name)
				delete(allUsers,user.id)
				message<-logoutInfo

				conn.Close()
				return 
			case <-restTimer:
				fmt.Printf("连接%s 重置计数器!\n",user.name)
		}
	}
}

创建并传入restTimer管道
在这里插入图片描述

在这里插入图片描述
效果:
在这里插入图片描述
最终代码

package main

import (
	"fmt"
	"net"
	"strings"
	"time"
)

type User struct {
	// 名字
	name string 
	// 唯一 的 id
	id string
	// 管道
	msg chan string

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

// 定义一个message全局通道,用于接收任何人发送过来消息
var message = make(chan string,10)

func main(){
	// 创建服务器
	listener,err := net.Listen("tcp",":8087")
	if err != nil{
		fmt.Println("net.Listen err:",err)
		return
	}
	// 启动全局唯一的go程,负责监听message通道,写给所有的用户
	go broadcast()

	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("==>主go程监听中......")
		// 监听
		conn,err := listener.Accept()
		if err != nil{
			fmt.Println("listener.Accept err:",err)
			return
		}

		// 建立连接
		fmt.Println("建立连接成功!")
		// 启动处理业务的go程
		go handler(conn)

	}
}

// 处理具体业务
func handler(conn net.Conn){

	fmt.Println("启动业务...")
	// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
	clientAddr := conn.RemoteAddr().String()
	fmt.Println("clientAddr:",clientAddr)
	// 创建user
	newUser := User{
		id:clientAddr,// id 我们不会修改,这个作为map中的key
		name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
		msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
	}
	// 添加user到map结构
	allUsers[newUser.id] = newUser


	// 定义一个退出信号,用来监听client退出
	var isQuit = make(chan bool)
	// 创建一个用于重置计数器的管道,用于告知watch函数,当前用户正在输入
	var restTimer = make(chan bool)
	// 启动go程,负责监听退出信号
	go watch(&newUser,conn,isQuit,restTimer)

	// 启动go程,负责将msg的信息返回给客户端
	go writeBackToClient(&newUser,conn)

	// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
	loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!\n",newUser.id,newUser.name)
	message <- loginInfo

	for{
		// 具体业务逻辑
		buf := make([]byte,1024)

		// 读取客户端发送来的数据
		cnt,err := conn.Read(buf)
		if cnt == 0 {
			fmt.Println("客户端主动关闭ctrl+c,准备退出!")
			// map 删除,用户 conn  close掉
			// 服务器还可以主动的退出
			// 在这里不进行真正的退出动作,而是发出一个退出信号,统一做退出处理,可以使用新的管道来做信号传递
			isQuit <- true
		}

		if err != nil{
			fmt.Println("listener.Read err:",err)
			return
		}
		fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
		// -------------业务逻辑处理  开始-------------	
		// 1.查询当前所有的用户 who
		// a. 先判断接收的数据是不是who  ==》 长度&&字符串
		userInput := string(buf[:cnt-1])  // 这是用户输入的数据,最后一个是回车,我们去掉他
		if len(userInput) == 3 && userInput == "who"{
			// b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端
			fmt.Println("用户即将查询所有用户信息!")
			// 这个切片包含所有的用户信息
			var userInfos []string
			for _,user := range allUsers{
				userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
				userInfos = append(userInfos,userInfo)
			}

			// 最终写入到通道中,一定是一个字符串
			r := strings.Join(userInfos,"\n")

			// 将数据返回到客户端
			newUser.msg <- r
		}else if len(userInput) > 8 && userInput[:6] == "rename"{

			// 规则:rename|Duke
		 //     获取数据判断长度7,判断字符是rename
		 //     使用|进行分割,获取|后面的部分,作为名字
		 //     更新用户名字newUser.name = Duke
			newUser.name = strings.Split(userInput,"|")[1]
			allUsers[newUser.id] = newUser // 更新map中的user
		 //     通知客户端,更新成功
			message <- userInput
		}else{
			// 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发
			message <- userInput
		}
		restTimer <- true
		// -------------业务逻辑处理  结束-------------	

	}
	
}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
	fmt.Println("广播go程启动成功...")
	defer fmt.Println("broadcast 程序退出!")

	for {
		// 1. 从message中读取数据
		fmt.Println("broadcast监听message中...")
		info := <-message 
		// 2. 将数据写入到每一个用户的msg管道中
		for _,user := range allUsers{
			// 如果msg是非缓冲,那么会在这里阻塞
			user.msg <- info 
		}
	}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
	fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
	for data := range user.msg{
		fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

		// Write(b []byte)(n int,err error)
		_,_ = conn.Write([]byte(data))
	}
}

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){
	fmt.Println("启动监听信号退出的go程...")
	defer fmt.Println("watch go程退出!")
	for{
		select{
			case <-isQuit:
				logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
				fmt.Println("删除当前用户:",user.name)
				delete(allUsers,user.id)
				message<-logoutInfo

				conn.Close()
				return
			case <-time.After(10*time.Second):
				logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name)
				fmt.Println("删除当前用户:",user.name)
				delete(allUsers,user.id)
				message<-logoutInfo

				conn.Close()
				return 
			case <-restTimer:
				fmt.Printf("连接%s 重置计数器!\n",user.name)
		}
	}
}

这里还有问题就是,上锁问题。记得在操作map的时候加上读锁和写锁
案例

package main

import(
	"fmt"
	"sync"
	"time"
)

var idnames = make(map[int]string)
var lock sync.RwMutex

// map不允许同事读写,如果有不同go程同时操作map,需要对map上锁

func main(){
	go func(){
		for{
			lock.lock()
			idnames[0] = "duke"
			lock.Unlock()
		}
	}()

	go func(){
		for{
			lock.Lock()
			name := idnames[0]
			fmt.Println("name:",name)
			lock.Unlock()
		}
	}()

	for{
		fmt.Println("OVER")
		time.Sleep(1*time.Second)
	}
}

感谢大家观看,我们下次见

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

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

相关文章

Notion 汉化Macwindows客户端

1、注册/登录账号&#xff1a; https://www.notion.so/zh-cn 2、下载桌面应用&#xff1a; https://www.notion.so/desktop 3、下载汉化js插件 地址&#xff1a;https://github.com/Reamd7/notion-zh_CN 点击最后一次更新的标签&#xff0c;下载【 notion-zh_CN.js 】文件 …

[激光原理与应用-64]:激光器-器件 - 光电二极管

第1章 概述光电二极管&#xff08;Photo-Diode&#xff09;和普通二极管一样&#xff0c;也是由一个PN结组成的半导体器件&#xff0c;也具有单方向导电特性。但在电路中它不是作整流元件&#xff0c;而是把光信号转换成电信号的光电传感器件。普通二极管在反向电压作用时处于截…

如何在Windows上同时搭建多个版本的golang环境——g

假如说我们不同的项目使用的go版本是不一样的&#xff0c;当我们想切换时&#xff0c;不懂的人可能就直接卸载掉现有的go环境去安装新的go环境了&#xff0c;这种方法可行但是有点呆&#xff0c;今天推荐一个好用的go版本管理工具——g,不是寄了的寄&#xff0c;就是英文字母g。…

jQuery(一):选择器、样式操作、动画效果

jQuery选择器样式操作jQuery 动画效果选择器 1.基础选择器 语法格式&#xff1a; $(“选择器”) // 里面选择器直接写 CSS 选择器即可&#xff0c;但是要加引号 常见选择器类型 例如&#xff1a; 2.样式设置 语法格式&#xff1a; $(‘div’).css(‘属性’, ‘值’) 例如&…

PHP 百度知识图谱数据处理与接入SDK

前言最近的项目在做百度知识图谱相关的&#xff0c;需要实现内容将本地数据处理->数据格式转化->数据接入图谱平台。因为百度提供的sdk为Python的sdk&#xff0c;所以我需要将Python程序转换成PHP格式。图谱生产流程介绍1.知识定义建立相应的scheme&#xff0c;确定类目以…

探究SQL SERVER 更改跟踪

介绍SQL SERVER 在2008 以上的版本提供两个用于数据库中跟踪数据更改的功能&#xff1a;变更数据捕获&#xff08;CDC &#xff09;与更改跟踪&#xff08;CT &#xff09;。这两个功能使应用程序能够确定对数据库中的用户表所做的 DML 更改&#xff08;插入、更新和删除操作 &…

Python基础(二十五):异常处理基础知识

文章目录 异常处理基础知识 一、了解异常 二、异常的写法 1、语法

HNUCM蓝桥杯Python组寒假第二次练习

文章目录1316: 选房子2158: 倍数问题2196: X星游戏1034: 近似回文词1749: 最少硬币2079: X星大学2086: 奖牌榜总结&#xff1a;1316: 选房子 1316: 选房子 [命题人 : 外部导入] 时间限制 : 1.000 sec 内存限制 : 128 MB 题目描述 栋栋和李剑已经大四了&#xff0c;想要出去找…

UDS诊断系列介绍13-31服务

本文框架1. 系列介绍1.1 31服务概述2. 31服务请求与应答2.1 31服务请求2.2 31服务正响应2.3 31服务否定响应3. Autosar系列文章快速链接1. 系列介绍 UDS&#xff08;Unified Diagnostic Services&#xff09;协议&#xff0c;即统一的诊断服务&#xff0c;是面向整车所有ECU的…

”凌寒独自开“绽放不一样的自己

目录 1&#xff1a;介绍 2&#xff1a;细节介绍 ​编辑 3&#xff1a;范例演示 4&#xff1a;平台助力 5&#xff1a;冲冲冲 1&#xff1a;介绍 每一个程序员都有一个产品的梦想&#xff0c;在独自开&#xff0c;每一位开发者为自己写代码。 毕竟谁又想错过每一个展示自…

CSDN网站勋章获取介绍

持之以恒&#xff1a;已下线&#xff0c;目前无法获取 签到勋章&#xff1a;签到新秀、签到达人、签到王者都已下线&#xff0c;目前无法获取 分享勋章&#xff1a;分享学徒、分享小兵、分享达人、分享精英、分享宗师、分享王者、至尊王者都已下线&#xff0c;目前无法获取 …

Git Extensions的安装与使用

一&#xff0c;介绍&#xff1a; Git Extensions是一个工具包&#xff0c;旨在使Windows下的Git更直观。 功能 Git的Windows资源管理器集成 功能丰富的Git用户界面 32位和64位支持 二&#xff0c;安装 csdn下载地址 GitExtensionhttps://download.csdn.net/download/weixin…

前端面试当中 js原型及原型链常考知识点

一、构造函数 讲原型则离不开构造函数&#xff0c;让我们先来认识下构造函数。 1.1 构造函数分为 实例成员 和 静态成员 让我们先来看看他们分别是什么样子的。 实例成员&#xff1a; 实例成员就是在构造函数内部&#xff0c;通过this添加的成员。实例成员只能通过实例化的…

【MyBatis】第三篇:传递参数

前面聊了一些配置文件的标签意义&#xff0c;在实例演示的时候&#xff0c;发现用的sql都是定死的&#xff0c;也就是无法传递参数&#xff0c;这个不符合开发的环境的&#xff0c;毕竟程序是需要交互的&#xff0c;自然也会有参数传递&#xff0c;下面依次进行演示。 在映射文…

一个注解(优雅)搞定SpringBoot项目中的身份证号、手机号等敏感数据脱敏

&#x1f4de; 文章简介&#xff1a;一个注解搞定身份证号、手机号等敏感数据脱敏处理 &#x1f4a1; 创作目的&#xff1a;公司中CRM的第一版快封版了&#xff0c;基本功能都落实了。接下来就是预防等保了&#xff0c;我负责的是核心业务客户模块这里自然有很多数据要做脱敏。…

【十 三】Netty 私有协议栈开发

Netty 私有协议栈开发私有协议介绍跨节点通信私有协议栈设计私有协议栈的网络拓扑图协议栈功能描述协议的通信模型消息体定义消息定义表私有协议消息头定义私有协议支持的字段类型私有协议的编解码规范私有协议的编码私有协议的解码链路的建立客户端握手请求握手请求消息定义服…

智能上线阻断的算法和实践

一、背景 上线是引起系统故障的主要原因之一&#xff0c;在有些公司甚至能占到故障原因的一半&#xff0c;及时地发现并阻断、回滚存在故障隐患的上线&#xff0c;可以有效地减少系统故障。 事实上&#xff0c;现在很多平台已经提供了基于手工配置规则来进行变更阻断的能力。…

Modern C++ | 谈谈万能引用以及它的衍生问题:将亡值、引用折叠和完美转发

文章目录前言左右值引用的铺垫万能引用&&引用折叠完美转发前言 在学习Linux系统编程的过程中&#xff0c;想着得到了新知识&#xff0c;不能把旧知识忘了啊&#xff0c;所以我就读起了以前写的博客&#xff0c;在Modern C介绍这篇博客中&#xff0c;关于完美转发只是介…

安科瑞智能仪表在密集母线行业中的应用

安科瑞 华楠AMB100智能母线监控系列系统示意图功能1.电压、电流、频率、有功功率、无功功率、功率因数、有功电能、无功电能、2-63次谐波、温度、湿度、漏电流等电参量测量&#xff1b;2.交流2DI、2DO&#xff0c;直流4DI、2DO&#xff1b;3.可信号取电或者独立辅助电源供电&am…

windows安装VMware最新版本(VMware Workstation 17.0 Pro)详细教程

目录 一、概述 二、下载 VMware Workstation 17.0 Pro 三、安装 VMware Workstation 17.0 Pro 四、创建一个空的虚拟机 一、概述 VMware Workstation Pro™ 是一个运行在window或Linux系统的软件&#xff0c;使开发人员能够在同一台 PC 上同时运行多个基于 x86 的 Windows、Li…