览器解析HTML文件的过程是网页呈现的关键步骤之一。具体介绍如下:
HTML文档的接收和预处理
解析为DOM树
CSS解析与CSSOM树构建
JavaScript加载与执行
渲染树的构建
布局计算(Layout)
绘制(Paint)
因此,我们开发中要注意以下几点:
综上所述,浏览器解析HTML文件是一个复杂而高度优化的过程,涉及从网络获取HTML文档到最终将其渲染到屏幕上的多个步骤。开发者需要深入理解这些步骤,以优化网页性能和用户体验。通过合理组织HTML结构、优化资源加载顺序、减少不必要的DOM操作和合理安排CSS和JavaScript的加载与执行,可以显著提升页面加载速度和运行效率。
言:
本人最近打算开始学习web开发了,每天写一点笔记,如果需要的话可以留个参考,如果没有人需要,我就当自己记笔记了,如果哪里有问题 欢迎各位高手评论区留言指点,感谢。
笔记正文:
书写文本有文本格式,编写网页的时候,html 也有自己的基本结构或基本格式,它是这样的:↓
大标签包含小标签,小标签内对应不同的内容,而这些标签的分级结构就是 父、子 关系,并且层级之间是靠缩进来区分,越靠外的为父。
计算机网络中的OSI(Open Systems Interconnection)七层模型是一种理论框架,用于描述计算机网络中数据通信的过程。OSI模型将计算机网络通信过程划分为七个层次,每个层次都有其特定的功能和协议。这种分层结构有助于研究和理解计算机网络中的通信原理。以下是OSI七层模型的各个层次及其主要功能:
应用层是OSI模型的第七层,也是网络应用程序和网络协议之间的接口。应用层主要负责为用户提供各类应用服务,如文件传输、电子邮件、Web浏览等。
表示层是OSI模型的第六层,主要负责处理在网络中传输的数据的表示方式,如数据加密、解密、压缩、解压缩等。表示层确保了不同系统之间的数据兼容性。
会话层是OSI模型的第五层,主要负责建立、维护和终止应用程序之间的通信会话。会话层提供了数据交换的同步和确认机制。
传输层是OSI模型的第四层,主要负责在源主机和目标主机之间提供可靠的、端到端的数据传输服务。传输层通过分段、封装和重组数据来实现可靠的数据传输。常见的传输层协议包括TCP(传输控制协议)和UDP(用户数据报协议)。
网络层是OSI模型的第三层,主要负责将数据包从源主机路由到目标主机。网络层主要负责逻辑寻址、路由选择和分组转发。常见的网络层协议包括IP(互联网协议)和ICMP(互联网控制报文协议)。
数据链路层是OSI模型的第二层,主要负责将网络层传来的数据包封装成帧(Frame),并在同一局域网内进行传输。数据链路层主要负责物理寻址、数据成帧、错误检测和流量控制。常见的数据链路层协议包括以太网(Ethernet)、令牌环(Token Ring)和无线局域网(Wi-Fi)等。
物理层是OSI模型的第一层,主要负责在物理介质上实现比特流的透明传输。物理层主要关注硬件接口、电气特性、光纤、无线传输等方面的问题。
OSI七层模型提供了一个通用的框架,帮助研究和理解计算机网络中的通信原理。实际应用中,我们通常使用TCP/IP四层模型,它包括了应用层、传输层、网络层和链路层,与OSI模型有一定的对应关系。
HTML框架进行分层设计的主要原因是为了提高代码的可读性、可维护性和可重用性。将HTML框架分层可以提高整体项目的结构和逻辑,便于开发者更好地理解和修改代码。分层设计具有以下优点:
HTML框架包括Application层``middleware层``route层``codec层``transport层 Application层 应用层通常包括与业务逻辑相关的代码,如Web应用程序的控制器(Controller)、视图(View)和模型(Model)。应用层的主要作用是处理用户请求并返回相应的响应。
Middleware层 中间件层是介于应用层和底层框架之间的一层,负责处理一些通用的功能,如身份验证、授权、缓存、日志记录等。中间件层有助于将业务逻辑与通用功能分离,使得应用层更加简洁和易于维护。
Route层 路由层负责处理HTTP请求的URL和HTTP方法(如GET、POST等),将请求分发到相应的控制器和方法。路由层的主要作用是根据URL映射来定位具体的功能代码。
Codec层 编解码层负责处理数据的编码和解码。在Web开发中,编码和解码通常涉及到HTML、CSS、JavaScript等前端技术的处理,以及JSON、XML等数据交换格式的处理。编解码层的主要作用是将数据转换为特定的格式,以便在不同层之间进行传输和处理。
Transport层 传输层负责处理底层的网络通信,如TCP、UDP等协议的使用。在Web开发中,传输层通常涉及到HTTP协议的处理,包括请求和响应的创建、发送和接收。传输层的主要作用是确保数据的可靠传输和在网络中的正确路由。
这些层次在实际应用中可能因框架和场景的不同而有所差异。但是,从您提供的描述来看,它们分别负责处理Web应用程序中的不同功能,共同构成了一个完整的Web开发框架。
应用层设计主要是设置各种接口,用于路由使用。
例如在字节后端进阶版中的大项目中的注册接口。
新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token.
接口类型
POST
接口定义
go复制代码syntax = "proto2";
package douyin.core;
message douyin_user_register_request {
required string username = 1; // 注册用户名,最长32个字符
required string password = 2; // 密码,最长32个字符
}
message douyin_user_register_response {
required int32 status_code = 1; // 状态码,0-成功,其他值-失败
optional string status_msg = 2; // 返回状态描述
required int64 user_id = 3; // 用户id
required string token = 4; // 用户鉴权token
}
go复制代码func Register(username, password string) (id int64, token int64, err error) {
if len(username) > 32 {
return 0, 0, errors.New("用户名过长,不可超过32位")
}
if len(password) > 32 {
return 0, 0, errors.New("密码过长,不可超过32位")
}
// 先查布隆过滤器,不存在直接返回错误,降低数据库的压力
if userNameFilter.TestString(username) {
return 0, 0, errors.New("用户名已经存在!")
}
//雪花算法生成token
node, err := snowflake.NewNode(1) //这里的userIdInt64就是 User.Id(主键)
if err != nil {
log.Println("雪花算法生成id错误!")
log.Println(err)
}
token1 := node.Generate().Int64()
tokenStr := strconv.FormatInt(token1, 10)
user := domain.User{}
// 再查缓存
data, err := dao.RedisClient.Get(context.Background(), tokenStr).Result()
if err == redis.Nil {
fmt.Println("token does not exist")
} else if err != nil {
fmt.Println("Error:", err)
} else {
num, err := strconv.ParseInt(data, 10, 64)
if err != nil {
fmt.Println("Error:", err)
return num, 0, err
}
return num, token1, nil
}
//在查数据库
user = domain.User{}
dao.DB.Model(&domain.User{}).Where("name = ?", username).Find(&user)
if user.Id != 0 {
return 0, 0, errors.New("用户已存在")
}
user.Name = username
// 加密存储用户密码
user.Salt = randSalt()
buf := bytes.Buffer{}
buf.WriteString(username)
buf.WriteString(password)
buf.WriteString(user.Salt)
pwd, err1 := bcrypt.GenerateFromPassword(buf.Bytes(), bcrypt.MinCost)
if err1 != nil {
return 0, 0, err
}
user.Pwd = string(pwd)
//存在mysql里边
dao.DB.Model(&domain.User{}).Create(&user)
//再把用户id作为键 用户的所有信息作为值存在其中
//用户信息的缓存是 保存在redis中 一个以id为键 user json为值
jsonuser, err1 := MarshalUser(user)
if err1 != nil {
fmt.Println("err1", err1)
return 0, 0, err1
}
err = dao.RedisClient.Set(context.Background(), strconv.FormatInt(user.Id, 10), jsonuser, 0).Err()
if err != nil {
fmt.Println("err", err)
return 0, 0, err
}
// 布隆过滤器中加入新用户
userIdFilter.AddString(strconv.FormatInt(user.Id, 10))
userNameFilter.AddString(username)
return user.Id, token1, nil
}
本接口注册功能实现:把所有信息存在mysql里边当然redis里边也存在这些信息,当然username也存在了布容过滤器中去,当接收到用户的username的时候我们现在布容过滤器中先查询是否存在如果存在则直接返回err,不存在然后再在redis里边查询,因为redis相比于mysql是更为轻量级的所以我们要先在redis里边进行查,如果查不到再进mysql里边查去,查不到说明没有注册过,可以注册。
遵循命名规范原则。
gin框架里的中间件分为全局中间件,局部中间件。那么什么是中间件?中间件是为应用提供通用服务和功能的软件。数据管理、应用服务、消息传递、身份验证和 API 管理通常都要通过中间件。在gin框架里,就是我们的所有API接口都要经过我们的中间件,我们可以在中间件做一些拦截处理。
这个是在服务启动就开始注册,全局意味着所有API接口都会经过这里。Gin的中间件是通过Use方法设置的,它接收一个可变参数,所以我们同时可以设置多个中间件。
首先定义如下
go复制代码// 1.创建路由
r := gin.Default() //默认带Logger(), Recovery()这两个内置中间件
r:= gin.New() //不带任何中间件
// 注册中间件
r.Use(MiddleWare())
r.Use(MiddleWare2())
注意的是
gin.Default()默认使用了Logger和Recovery中间件,其中:Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
go复制代码// 定义中间
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置变量到Context的key中,可以通过Get()取
c.Set("request", "这是中间件设置的值")
status := c.Writer.Status()
fmt.Println("中间件执行完毕", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
然后启动我们的服务,访问任意一个接口可以看到输出如下
这是请求先到了中间件,然后在到我们的API接口。在中间件里可以设置变量到Context的key中,然后在我们的API接口取值。
go复制代码 r.GET("/", func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
这时候在访问就可以看到中间件设置的值是
next方法是在中间件里面使用,这个是执行后续中间件请求处理的意思(含没有执行的中间件和我们定义的GET方法处理,如果连续注册几个中间件则会是按照顺序先进后出的执行,遇到next就去执行下一个中间件里的next前面方法。
go复制代码 // 执行函数
c.Next()
// 中间件执行完后续的一些事情
局部中间件意味着部分接口才会生效,只在局部使用,这时候访问http:127.0.0.1:8000/ 才会看到中间件的日志打印,其他API接口则不会出现。
go复制代码 //局部中间件使用
r.GET("/", MiddleWare(), func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
go复制代码
func BasicAuth(accounts Accounts) HandlerFunc
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc
func ErrorLogger() HandlerFunc
func ErrorLoggerT(typ ErrorType) HandlerFunc
func Logger() HandlerFunc
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc
func WrapH(h http.Handler) HandlerFunc
通过自定义中间件,我们可以很方便的拦截请求,来做一些我们需要做的事情,比如日志记录、授权校验、各种过滤等等。
Gin 是一个标准的 Web 服务框架,遵循 Restful API 接口规范,其路由库是基于 httproute 实现的。
本节将从 Gin 路由开始,详细讲述各种路由场景下,如何通过 Gin 来实现。
示例
字节大项目注册接口
go复制代码syntax = "proto2";
package douyin.core;
message douyin_user_register_request {
required string username = 1; // 注册用户名,最长32个字符
required string password = 2; // 密码,最长32个字符
}
message douyin_user_register_response {
required int32 status_code = 1; // 状态码,0-成功,其他值-失败
optional string status_msg = 2; // 返回状态描述
required int64 user_id = 3; // 用户id
required string token = 4; // 用户鉴权token
}
go复制代码func Register(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
id, token, err := service.Register(username, password)
if err != nil {
c.JSON(http.StatusOK, domain.Response{StatusCode: 1, StatusMsg: err.Error()})
} else {
c.JSON(http.StatusOK, domain.UserLoginResponse{
//可以直接去掉
Response: domain.Response{StatusCode: 0},
Id: id,
Token: token,
})
}
}
go复制代码package main
import (
"github.com/gin-gonic/gin"
"github.com/goTouch/TicTok_SimpleVersion/controller"
)
func initRouter(r *gin.Engine) {
// public directory is used to serve static resources
r.Static("/static", "./public")
apiRouter := r.Group("/douyin")
// basic apis
//controller.VerifyToken,
apiRouter.POST("/user/", controller.UserInfo)
apiRouter.POST("/user/register/", controller.LoginLimit, controller.Register)
apiRouter.POST("/user/login/", controller.LoginLimit, controller.Login)
}
在Web开发中,编码和解码通常涉及到HTML、CSS、JavaScript等前端技术的处理,以及JSON、XML等数据交换格式的处理。编解码层的主要作用是将数据转换为特定的格式,以便在不同层之间进行传输和处理。
示例
在postman中的示例 json
xml
html复制代码{"status_code":1,"status_msg":"redis: nil"}
{"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
Text复制代码{"status_code":1,"status_msg":"redis: nil"}
{"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
Auto复制代码{
"status_code": 1,
"status_msg": "redis: nil"
}{
"status_code": 2,
"status_msg": "no multipart boundary param in Content-Type"
}
传输层负责处理底层的网络通信,如TCP、UDP等协议的使用。在Web开发中,传输层通常涉及到HTTP协议的处理,包括请求和响应的创建、发送和接收。传输层的主要作用是确保数据的可靠传输和在网络中的正确路由。
golang语言中net/http这个库中的conn 他是BIO自带阻塞
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
BIO通信(一请求一应答)模型图如下(图源网络,原出处不明):
采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。
如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。
我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?
程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 golang实现BIO
NIO: NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。
————————————————
NIO:模型图
Reactor模型:
学习NIO先来搞清楚一些相关的概念,NIO通讯有哪些相关组件,对应的作用都是什么,之间有哪些联系?
首先我们来了解下传统的Socket网络通讯模型。
传统Socket通讯原理图
每次一个客户端接入,都是要在服务端创建一个线程来服务这个客户端的,这会导致大量的客户端的时候,服务端的线程数量可能达到几千甚至几万,几十万,这会导致服务器端程序负载过高,不堪重负,最终系统崩溃死掉。
NIO原理图
NIO的线程模型 对Socket发起的连接不需要每个都创建一个线程,完全可以使用一个Selector来多路复用监听N多个Channel是否有请求,该请求是对应的连接请求,还是发送数据的请求,这里面是基于操作系统底层的Select通知机制的,一个Selector不断的轮询多个Channel,这样避免了创建多个线程,只有当莫个Channel有对应的请求的时候才会创建线程,可能说1000个请求, 只有100个请求是有数据交互的, 这个时候可能server端就提供10个线程就能够处理这些请求。这样的话就可以避免了创建大量的线程。
NIO中的Buffer是个什么东西 ?
学习NIO,首当其冲就是要了解所谓的Buffer缓冲区,这个东西是NIO里比较核心的一个部分,一般来说,如果你要通过NIO写数据到文件或者网络,或者是从文件和网络读取数据出来此时就需要通过Buffer缓冲区来进行。Buffer的使用一般有如下几个步骤:
写入数据到Buffer,调用flip()方法,从Buffer中读取数据,调用clear()方法或者compact()方法。
capacity: 缓冲区容量的大小,就是里面包含的数据大小。
limit: 对buffer缓冲区使用的一个限制,从这个index开始就不能读取数据了。
position: 代表着数组中可以开始读写的index, 不能大于limit。
mark: 是类似路标的东西,在某个position的时候,设置一下mark,此时就可以设置一个标记,后续调用reset()方法可以把position复位到当时设置的那个mark上去,把position或limit调整为小于mark的值时,就丢弃这个mark。如果使用的是Direct模式创建的Buffer的话,就会减少中间缓冲直接使用的是DirectorBuffer来进行数据的存储。
————————————————
NIO中,Channel是什么?
Channel是NIO中的数据通道,类似流,但是又有些不同,Channel即可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的。Channel可以异步的读写。Channel中的数据总是要先读到一个Buffer中,或者从缓冲区中将数据写到通道中。
FileChannel的作用是什么 Buffer有不同的类型,同样Channel也有好几个类型。 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。而FileChannel就是文件IO对应的管道, 在读取文件的时候会用到这个管道。 golang的NIO
*请认真填写需求信息,我们会在24小时内与您取得联系。