【Go学习实战】03-3-文章评论及写文章
【Go学习实战】03-3-文章评论及写文章
- 文章评论
- 注册valine
- 获取凭证
- 加载评论页面
- 写文章
- 修改cdn位置
- 完善功能
- 查看页面
- 发布文章
- POST发布文章
- 发布文章测试
- 查询文章详情
- 查询详情测试
- 修改文章
- 修改文章测试
- 写文章图片上传
- 前端
- 后端逻辑
- 测试
文章评论
这里我们的博客因为是个轻量级的博客,所以评论系统我们选择Valine来作为我们的评论系统,我们只需要配置好对应的appid及appkey,之后调用api就可以进行使用。
注册valine

获取凭证
创建应用

点击应用凭证

将其配置到我们的config.toml中

配置好后重新加载
记得修改html页面的valine的cdn配置,使用官方的cdn
<script src="https://cdn.jsdelivr.net/npm/valine@latest/dist/Valine.min.js"></script>
加载评论页面

成功加载到评论,我们再新添几个评论

这里我们可以学习到评论一般都是放在mongodb的,因为这种评论是非结构化的,可能有图片,可能有文字,甚至可能有语音或者视频,并且评论的增长量也是非常恐怖的,所以我们可以把评论放在mongodb中进行存储。
写文章
修改cdn位置
在write.html中
<script src="{{.CdnURL}}/js/cos-js-sdk-v5.min.js"></script>
改为
<script src="https://cdn.jsdelivr.net/npm/cos-js-sdk-v5/dist/cos-js-sdk-v5.min.js"></script>
完善功能
分配路由:
写作请求的url是http://localhost:8080/writing,我们也要对其分配路由,因为是页面,所以是view下的
http.HandleFunc("/writing", views.HTML.Writing)
在views中完善我们的功能
创建对应的解析路径和接口
接口
type HTMLRenderer interface {Index(w http.ResponseWriter, r *http.Request)Category(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)Detail(w http.ResponseWriter, r *http.Request)Writing(w http.ResponseWriter, r *http.Request)
}
解析路径,创建writing.go
func (*HTMLApi) Writing(w http.ResponseWriter, r *http.Request) {writing := common.Template.Writingwr := service.Writing()writing.WriteData(w, wr)
}
这个数据我们要定义一下,我们对照写的前端完善一下要传回取页面的数据,在models/article.go中
type WriteRes struct {Title string `json:"title"`CdnURL string `json:"cdnURL"`Categorys []Category `json:"categorys"`
}
完善service层,在service/detail.go中
func Writing() (wr models.WriteRes) {wr.Title = config.Cfg.Viewer.Titlewr.CdnURL = config.Cfg.System.CdnURLcategorys, err := dao.GetAllCategory()if err != nil {log.Printf("查询分类异常: %v", err)return}wr.Categorys = categorysreturn
}
查看页面
因为要加载cdn中的css样式文件,我们使用国内的镜像cdn
// 使用国外的CDN,加载速度有时会很慢,或者自定义URL
// You can custom KaTeX load url.
editormd.katexURL = {css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",js : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"
};
修改为
editormd.katexURL = {css: "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css",js: "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"
};
因为css样式文件都在本地,所以我们直接把CDN的url指向本地就好,url为resource,然后路由到public/resource就好
[system]CdnURL = "/resource"
我们的编辑器使用的markdown

发布文章
我们写完了之后点击发布请求的url是http://localhost:8080/api/v1/post,我们也要对其分配路由
http.HandleFunc("/api/v1/post", api.API.SaveAndUpdatePost)
接口
type APIResponder interface {SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)
}
因为是返回json,所以是api下的
因为我们后期不仅有发布文章POST还是修改文章PUT,我们要分别处理
因为我们不管在什么时候都要验证下用户是否登录,所以要检查token
//判断用户是否登录
token := r.Header.Get("Authorization")
if token == "" {log.Printf("SaveAndUpdatePost的token为空")return
}
//解析token
_,claim,err:=utils.ParseToken(token)
if err!=nil{common.ErrorResult(w,errors.New("token解析失败"))return
}
uid:=claim.Uid
所以我们通过对r *http.Request的解析获取他的Method,再根据是什么样的请求类型分别进行处理
method := r.Methodswitch method {case http.MethodPost:params, err := common.GetRequestJsonParam(r)if err != nil {log.Printf("SaveAndUpdatePost的POST解析请求参数异常:%v", err)return}
POST发布文章
因为发起的是post请求,所以我们还是要解析表单,就要用到我们之前在common写的解析表单的方法
func GetRequestJsonParam(r *http.Request) (map[string]interface{}, error) {var params map[string]interface{}// 使用 json.NewDecoder 来逐步解码请求体decoder := json.NewDecoder(r.Body)err := decoder.Decode(¶ms)if err != nil {log.Printf("解析请求参数失败:%v", err)return nil, err}return params, nil
}
我们一般保存后都会返回一个文章的pid
func SavePost(post *models.Post) {res, err := DB.Exec("insert into blog_post(title, content, markdown, category_id, user_id, view_count, type, slug,create_at,update_at) values(?,?,?,?,?,?,?,?,?,?)",post.Title, post.Content, post.Markdown, post.CategoryId, post.UserId, post.ViewCount, post.Type, post.Slug, post.CreateAt, post.UpdateAt)if err != nil {log.Printf("保存文章失败: %v", err)}id, _ := res.LastInsertId()post.Pid = int(id)
}
service层
func SavePost(post *models.Post) {dao.SavePost(post)
}
再上层
case http.MethodPost:params, err := common.GetRequestJsonParam(r)if err != nil {log.Printf("SaveAndUpdatePost的POST解析请求参数异常:%v", err)return}cId := params["categoryId"].(string)categoryId, _ := strconv.Atoi(cId)content := params["content"].(string)markdown := params["markdown"].(string)slug := params["slug"].(string)title := params["title"].(string)postType := 0if params["type"] != nil {postType, _ = params["type"].(int)}post := &models.Post{CategoryId: categoryId,Content: content,Markdown: markdown,Slug: slug,Title: title,Type: postType,UserId: uid,Pid: -1,ViewCount: 0,CreateAt: time.Now(),UpdateAt: time.Now(),}service.SavePost(post)common.SuccessResult(w, post)
发布文章测试

成功插入数据库
查询文章详情
我们写完了之后点击发布请求的url是http://localhost:8080/api/v1/post/29,是个GET请求,我们也要对其分配路由
http.HandleFunc("/api/v1/post/", api.API.GetPost)
接口
type APIResponder interface {SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)GetPost(w http.ResponseWriter, r *http.Request)
}
查看其js
function getArticleItem(id) {$.ajax({url: "/api/v1/post/" + id,type: "GET",contentType: "application/json",success: function (res) {if (res.code != 200) {initEditor();return alert(res.error);}ArticleItem = res.data || {};initActive();initEditor();},beforeSend: setAjaxToken,});
}
我们最后直接返回post到ArticleItem就可以了
dao层我们已经写过了
func GetPostById(pid int) (*models.Post, error) {row := DB.QueryRow("select * from blog_post where pid = ?", pid)var post models.Posterr := row.Scan(&post.Pid, &post.Title, &post.Content, &post.Markdown, &post.CategoryId, &post.UserId, &post.ViewCount, &post.Type, &post.Slug, &post.CreateAt, &post.UpdateAt)if err != nil {return nil, err}return &post, nil
}
service
func GetPostById(pid int) (*models.Post, error) {return dao.GetPostById(pid)
}
api,因为这是个get请求,我们直接截断找id就好
func (*Api) GetPost(w http.ResponseWriter, r *http.Request) {path := r.URL.PathpIdStr := strings.TrimPrefix(path, "/api/v1/post/")pid, err := strconv.Atoi(pIdStr)if err != nil {common.ErrorResult(w, errors.New("GetPost不识别此请求路径"))return}post, err := service.GetPostById(pid)if err != nil {common.ErrorResult(w, errors.New("GetPost查询出错"))return}common.SuccessResult(w, post)
}
查询详情测试

修改文章
如果在已存在的文章进行修改,那么我们这请求就变成了PUT,所以也要对其进行路由,因为我们之前已经把post写了,我们这里只用完善下put就好
在api/post.go#SaveAndUpdatePost中
case http.MethodPut:params, err := common.GetRequestJsonParam(r)if err != nil {log.Printf("SaveAndUpdatePost的PUT解析请求参数异常:%v", err)return}pid := params["pid"].(float64)pId := int(pid)post, _ := service.GetPostById(pId)if post == nil {common.ErrorResult(w, errors.New("SaveAndUpdatePost查询不到文章"))return}cId := params["categoryId"].(string)categoryId, _ := strconv.Atoi(cId)content := params["content"].(string)markdown := params["markdown"].(string)slug := params["slug"].(string)title := params["title"].(string)postType := 0if params["type"] != nil {postType, _ = params["type"].(int)}post.CategoryId = categoryIdpost.Content = contentpost.Markdown = markdownpost.Slug = slugpost.Title = titlepost.Type = postTypepost.UpdateAt = time.Now()service.UpdatePost(post)common.SuccessResult(w, post)
dao层
func UpdatePost(post *models.Post) {_, err := DB.Exec("update blog_post set title=?, content=?, markdown=?, category_id=?, user_id=?, view_count=?, type=?, slug=?, update_at=? where pid=?",post.Title, post.Content, post.Markdown, post.CategoryId, post.UserId, post.ViewCount, post.Type, post.Slug, post.UpdateAt, post.Pid)if err != nil {log.Printf("更新文章失败: %v", err)}
}
service
func UpdatePost(post *models.Post) {dao.UpdatePost(post)
}
修改文章测试

查看数据库

写文章图片上传
前端
修改我们的前端符合我们的要求
imageUploadCalback: function (files, cb) {let formData = new FormData();formData.append("file", files[0]);fetch("/api/v1/upload/oss", { // 这里是后端的文件上传接口method: "POST",body: formData}).then(response => response.json()).then(data => {if (data.code === 200) {// 上传成功,回调并回显 URLcb(data.data.url); // 将返回的 URL 传递给编辑器// 可以根据需求在其他地方显示该图片console.log("上传成功,图片 URL:", data.data.url);} else {// 上传失败,处理错误alert("上传失败:" + (data.error || "未知错误"));}}).catch(error => {console.error("上传失败", error);alert("上传失败,请重试!");});
},
后端逻辑
因为我们请求的地址是/api/v1/upload/oss,所以也要做路由
http.HandleFunc("/api/v1/upload/oss", api.API.UploadImage)
接口
type APIResponder interface {SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)GetPost(w http.ResponseWriter, r *http.Request)UploadImage(w http.ResponseWriter, r *http.Request)
}
接下来写我们api的逻辑
package apiimport ("fmt""math/rand""myWeb/common""myWeb/config""net/http""time""github.com/aliyun/aliyun-oss-go-sdk/oss"
)// 阿里云 OSS 配置
var (accessKeyID = config.Cfg.System.AccessKeyID // 你的 AccessKey IDaccessKeySecret = config.Cfg.System.AccessKeySecret // 你的 AccessKey Secretendpoint = config.Cfg.System.Endpoint // OSS EndpointbucketName = config.Cfg.System.BucketName // 你的 Bucket 名称
)// uploadImage 处理文件上传到阿里云 OSS
func (*Api) UploadImage(w http.ResponseWriter, r *http.Request) {// 创建 OSS 客户端client, err := oss.New(endpoint, accessKeyID, accessKeySecret)if err != nil {http.Error(w, "无法创建 OSS 客户端", http.StatusInternalServerError)return}// 获取 OSS Bucketbucket, err := client.Bucket(bucketName)if err != nil {http.Error(w, "无法访问 OSS Bucket", http.StatusInternalServerError)return}// 解析请求中的表单数据err = r.ParseMultipartForm(10 << 20) // 限制最大文件大小为 10MBif err != nil {http.Error(w, "解析表单数据失败", http.StatusBadRequest)return}// 获取文件file, _, err := r.FormFile("file") // 获取表单中名为 "file" 的文件if err != nil {http.Error(w, "获取文件失败", http.StatusBadRequest)return}defer file.Close()// 获取文件的扩展名ext := ".jpg" // 默认使用 .jpg// 假设获取文件扩展名,这里可以根据实际情况修改if fileHeader, _, err := r.FormFile("file"); err == nil {ext = getFileExtension(fileHeader)}// 生成唯一的文件名,使用时间戳和文件扩展名fileName := fmt.Sprintf("uploads/%d_%s", time.Now().Unix(), generateRandomString(8)+ext)// 将文件上传到 OSSerr = bucket.PutObject(fileName, file)if err != nil {http.Error(w, "文件上传到 OSS 失败", http.StatusInternalServerError)return}// 生成文件的 URLimageURL := fmt.Sprintf("https://%s.%s/%s", bucketName, endpoint, fileName)// 使用 SuccessResult 返回响应数据common.SuccessResult(w, map[string]string{"url": imageURL})
}// 获取文件扩展名
func getFileExtension(file interface{}) string {// 根据实际需要从文件获取扩展名return ".jpg" // 示例返回 jpg 后缀
}// 生成随机字符串,用于文件名
func generateRandomString(n int) string {const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"result := make([]byte, n)for i := range result {result[i] = letters[rand.Intn(len(letters))]}return string(result)
}
测试
成功

查看阿里云,阿里云也上传成功

相关文章:
【Go学习实战】03-3-文章评论及写文章
【Go学习实战】03-3-文章评论及写文章 文章评论注册valine获取凭证加载评论页面 写文章修改cdn位置完善功能查看页面 发布文章POST发布文章发布文章测试 查询文章详情查询详情测试 修改文章修改文章测试 写文章图片上传前端后端逻辑测试 文章评论 这里我们的博客因为是个轻量级…...
从零开始用AI开发游戏(一)
1. 核心玩法设计 核心目标:玩家需在随机生成的3D迷宫中寻找出口,躲避陷阱、收集道具、解开谜题。核心机制: 随机生成迷宫:每次游戏生成不同结构的迷宫(递归分割算法或深度优先搜索)。第一人称视角…...
AI-大模型中的流式输出与非流式输出
1.前言 在大模型API开发中,流式与非流式输出对应着两种不同的数据交互,在代码中stream中通过参数true与false来进行设定。 2.流式输出与非流式输出的原理 2.1.非流式输出-请求一次响应返回完整数据 非流式输出,传统的请求-响应模式…...
【HarmonyOS Next】鸿蒙加固方案调研和分析
【HarmonyOS Next】鸿蒙加固方案调研和分析 一、前言 根据鸿蒙应用的上架流程,本地构建app文件后,上架到AGC平台,平台会进行解析。根据鸿蒙系统的特殊设置,仿照IOS的生态闭环方案。只能从AGC应用市场下载app进行安装。这样的流程…...
树莓集团现状最新进展:宜宾园区业务有何新突破
树莓集团宜宾园区在当下取得了令人瞩目的最新进展和新突破。在技术创新方面,园区加大研发投入,成功攻克了多项关键技术难题。 例如,在人工智能图像识别技术上取得重大突破,该技术已应用于园区内的智能安防系统和工业生产检测环节…...
蓝桥杯javaB组备战第二天 题目 区间次方和 编号3382
这是一个前缀和问题,但是不同于以为前缀和问题 前缀和问题求解思路: 创建一个前缀数组 s[] ,存储输入的元素的a[1]到a[n]的和 及:s[1] s[i-1]a[i] ,i>1 这样比暴力算法的复杂度要低很多可以将 时间复杂度从O(q*n*m)下降到 O(n*mq) …...
SpringBoot设置过滤器(Filter)或拦截器(Interceptor)的执行顺序:@Order注解、setOrder()方法
Java Web 过滤器、拦截器、监听器,系列文章: (1)过滤器(Filter)的使用: 《Servlet过滤器(Filter)的使用:Filter接口、@WebFilter注释》 《SpringMVC使用过滤器(Filter)解决中文乱码》 《SpringBoot过滤器(Filter)的使用:Filter接口、FilterRegistrationBean类配…...
【git】补丁文件
项目中总有一些本地修改是既不能上传到远程分支又不能直接加入到.gitignore文件中的。 固然可以使用stash但它毕竟只是一种临时保存更改的机制,更适用于本地开发过程中需要频繁切换任务的场景。 如果想要共享代码更改,那还是补丁文件更合适一些。git d…...
linux自启动服务
在Linux环境中,systemd是一个系统和服务管理器,它为每个服务使用.service文件进行配置。systemctl是用于控制系统服务的主要工具。本文将详细介绍如何使用systemctl来管理vsftpd服务,以及如何设置服务自启动。 使用Systemd设置自启动服务 创…...
Yashan DB 对象管理
一、什么是数据库对象 数据库对象是数据库里面用来存储和指向数据的各种概念和结构的总称。数据库支持的对象包括: • 表:表是一个逻辑概念,是数据库组织管理数据的基本单位。 • 索引:索引是建立在表上的逻辑对象,索…...
《Android 平台架构系统启动流程详解》
目录 一、平台架构模块 1.1 Linux 内核 1.2 硬件抽象层 (HAL) 1.3 Android 运行时 1.4 原生 C/C 库 1.5 Java API 框架 1.6 系统应用 二、系统启动流程 2.1 Bootloader阶段 2.2 内核启动 2.3 Init进程(PID 1) 2.4 Zygote与System Serv…...
强化学习(赵世钰版)-学习笔记(3.最优策略与贝尔曼最优方程)
这是本章在课程中的位置,属于基础工具中的最后一章,主要讨论了最优状态值(Optimal State Value)与最优策略(Optimal Policy),并介绍了对应的计算方法-贝尔曼最优方程(Bellman Optima…...
六十天前端强化训练之第十一天之事件机制超详解析
欢迎来到编程星辰海的博客讲解 目录 一、事件模型演进史 1.1 原始事件模型(DOM Level 0) 1.2 DOM Level 2事件模型 1.3 DOM Level 3事件模型 二、事件流深度剖析 2.1 捕获与冒泡对比实验 2.2 事件终止方法对比 三、事件委托高级应用 3.1 动态元…...
调试正常 ≠ 运行正常:Keil5中MicroLIB的“量子态BUG”破解实录
调试正常 ≠ 运行正常:Keil5中MicroLIB的“量子态BUG”破解实录——从勾选一个选项到理解半主机模式,嵌入式开发的认知升级 📌 现象描述:调试与烧录的诡异差异 在线调试时 程序正常运行 - 独立运行时 设备无响应 ! 编译过程 0 Err…...
基于SpringBoot实现旅游酒店平台功能八
一、前言介绍: 1.1 项目摘要 随着社会的快速发展和人民生活水平的不断提高,旅游已经成为人们休闲娱乐的重要方式之一。人们越来越注重生活的品质和精神文化的追求,旅游需求呈现出爆发式增长。这种增长不仅体现在旅游人数的增加上࿰…...
ArcGIS Pro中字段的新建方法与应用
一、引言 在地理信息系统(GIS)的数据管理和分析过程中,字段操作起着至关重要的作用。 无论是进行地图制作、空间分析还是数据统计,字段都是承载属性信息的基本单元。 ArcGIS Pro作为一款功能强大的GIS软件,为用户提…...
c#面试题12
1.ApplicationPool介绍一下 c#里没有 2.XML 可扩展标记语言,一般以.xml文件格式的形式存在。可用于存储结构化的数据 3.ASP.NET的用户控件 将原始的控件,用户根据需要进行整合成一个新的控件 4.介绍一下code-Behind 即代码后置技术,就是…...
Matlab中快速查找元素索引号
1、背景介绍 在算法设计过程中,有时候需要从一维/二维数组中,快速查找是否某个元素,以及该元素所在的位置。如一维矩阵[1 2 3 4 5 6 6 7 8]所示,元素6所在的位置为6 7。 2、函数测试 matlab中函数find()可以快速查找到指定元素所…...
LabVIEW非线性拟合实现正弦波参数提取
LabVIEW的Nonlinear Curve Fit.vi基于Levenberg-Marquardt算法,能够实现非线性最小二乘拟合,包括正弦波三参数(幅值、频率、相位)的精确求解。该工具适用于非均匀采样、低信噪比信号等复杂场景,但需注意初始参数设置与…...
S19文件格式详解:汽车ECU软件升级中的核心镜像格式
文章目录 引言一、S19文件格式的起源与概述二、S19文件的核心结构三、S19在汽车ECU升级中的应用场景四、S19与其他格式的对比五、S19文件实例解析六、工具链支持与安全考量七、未来趋势与挑战结语引言 在汽车电子控制单元(ECU)的软件升级过程中,S19文件(也称为Motorola S-…...
Redis 缓存穿透、缓存击穿与缓存雪崩详解:问题、解决方案与最佳实践
目录 引言 1. 缓存穿透 1.1 什么是缓存穿透? 示例: 1.2 缓存穿透的原因 1.3 缓存穿透的解决方案 1.3.1 缓存空对象 1.3.2 布隆过滤器(Bloom Filter) 1.3.3 参数校验 2. 缓存击穿 2.1 什么是缓存击穿? 示例&…...
Mamba| Miniforge3 安装和配置
参考教程: B站 教程概要 安装最新的 Mamba,建议通过安装 Miniforge 来实现,因为 Miniforge 默认包含 Mamba。Miniforge 下载:建议使用南京大学镜像站mamba 设置镜像源:清华镜像源修改默认环境安装路径设置 pip 镜像&a…...
Qt入门笔记
目录 一、前言 二、创建Qt项目 2.1、使用向导创建 2.2、最简单的Qt应用程序 2.2.1、main函数 2.2.2、widget.h文件 2.2.3、widget.cpp文件 2.3、Qt按键Botton 2.3.1、创建一个Botton 2.3.2、信号与槽 2.3.3、按键使用信号与槽的方法 2.4、文件Read与Write-QFile类 2…...
C语言每日一练——day_4
引言 针对初学者,每日练习几个题,快速上手C语言。第四天。(连续更新中) 采用在线OJ的形式 什么是在线OJ? 在线判题系统(英语:Online Judge,缩写OJ)是一种在编程竞赛中用…...
下降路径最⼩和(medium)
题目描述: 给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(…...
redux_旧版本
reduxjs/toolkit(RTK)是 Redux 官方团队推出的一个工具集,旨在简化 Redux 的使用和配置。它于 2019 年 10 月 正式发布,此文章记录一下redux的旧版本如何使用,以及引入等等。 文件目录如下: 步骤 安装依…...
⭐算法OJ⭐经典题目分类索引(持续更新)
在编程竞赛和算法学习中,Online Judge(OJ)平台是程序员们磨练技能的重要工具。OJ平台上的题目种类繁多,涵盖了从基础数据结构到复杂算法的各个方面。为了更好地理解和掌握这些题目,对其进行分类是非常有必要的。这篇索…...
python之使用scapy扫描本机局域网主机,输出IP/MAC表
安装scapy库 pip install scapy -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple扫描本机局域网的所有主机,输出IP/MAC对于表 # -*- coding: UTF-8 -*- import netifaces from scapy.all import srp from scapy.layers.l2 import ARP, Ether import ipa…...
Spring Boot中@Valid 与 @Validated 注解的详解
Spring Boot中Valid 与 Validated 注解的详解 引言 在Spring Boot应用中,参数校验是确保数据完整性和一致性的重要手段。Valid和Validated注解是Spring Boot中用于参数校验的两个核心注解。本文将详细介绍这两个注解的用法、区别以及代码样例。 Valid注解 功能介…...
18、TCP连接三次握手的过程,为什么是三次,可以是两次或者更多吗【高频】
三次握手的过程: 第一次握手:客户端 向 服务器 发送一个 SYN(也就是同步序列编号报文),请求建立连接。随后,客户端 进入 SYN_SENT 状态;服务器收到 SYN 之后,由 LISTEN 状态变为 SYN…...
