这是我参与「第五届青训营」伴学笔记创作活动的第 7 天
前言:
本文是针对青训营中的极简版抖音example的一个解释;
缘笔者为项目小白,惑之众多,故撰写此文以明确之,若读者亦有相似之疑问,望可助之;
若笔者有任何疏忽纰漏之处,烦请不吝赐教。
文件目录结构解释:
- controller文件夹中包含程序的控制器代码,负责处理程序的业务逻辑和路由请求。
- public文件夹中包含程序的静态文件,如图片、视频、CSS和JavaScript文件。
- service文件夹中包含程序的服务代码,负责处理数据库操作和其他后台逻辑。
- test文件夹中包含程序的单元测试代码。
- go.mod文件是golang项目的依赖管理文件。
- go.sum文件是golang项目依赖的锁定文件。
- main.go文件包含程序的入口函数和一些初始化操作。
- router.go文件包含程序的路由定义和中间件配置。
明确概念:
- 控制器:控制器是一个模块,它负责处理与用户交互的请求,并根据请求执行适当的业务逻辑。在这个demo中,控制器是由
user.go文件实现的,它包含了处理注册、登录、用户信息请求的函数。 - 业务逻辑:业务逻辑是指处理特定业务需求的操作和流程。在这个demo中,业务逻辑是由控制器中的函数实现的,它们包括了验证用户名和密码、处理用户信息、生成登录令牌等。
- 路由请求:路由请求是指用户发送给服务器的请求。在这个demo中,路由请求是由
main.go文件实现的,它将用户请求映射到控制器中的对应函数上。 - 静态文件:静态文件是指不会改变的文件,如 HTML, CSS, JavaScript, 图片等。这些文件通常直接由客户端浏览器请求并下载,不需要经过服务器端的处理。在 web 应用中,通常将静态文件和动态文件分开存放,避免动态文件的处理对性能产生影响。
代码详解:
main.go
func main() {
// 启动一个 goroutine 来运行 service 包中的 RunMessageServer 函数
// 这个函数负责:运行消息服务器
go service.RunMessageServer()
// 创建一个 gin 框架的默认路由器,并将其赋值给变量 r
r := gin.Default()
// 这句话调用了 initRouter 函数,用来初始化并配置路由器 r
initRouter(r)
// 启动服务器并监听在 0.0.0.0:8080,当接收到请求时,使用配置好的路由器进行处理
// for windows "localhost:8080"
r.Run()
}
router.go
func initRouter(r *gin.Engine) {
// public目录用于提供静态资源
// 配置了路由器 r 使用 public 目录来服务静态资源,访问路径为 /static/xxx
r.Static("/static", "./public")
// 创建了一个子路由器,叫做 apiRouter,并且其中所有的路由都以 /douyin 开头
apiRouter := r.Group("/douyin")
// basic apis
// 配置了路径为 /douyin/feed 的 GET 请求路由,并将请求处理交给 controller 包中的 Feed 函数
apiRouter.GET("/feed/", controller.Feed)
apiRouter.GET("/user/", controller.UserInfo)
// 配置了路径为 /douyin/user/register 的 POST 请求路由,并将请求处理交给 controller 包中的 Register 函数 ,下面的类似,都是对应着不同的API接口
apiRouter.POST("/user/register/", controller.Register)
apiRouter.POST("/user/login/", controller.Login)
apiRouter.POST("/publish/action/", controller.Publish)
apiRouter.GET("/publish/list/", controller.PublishList)
// extra apis - I
apiRouter.POST("/favorite/action/", controller.FavoriteAction)
apiRouter.GET("/favorite/list/", controller.FavoriteList)
apiRouter.POST("/comment/action/", controller.CommentAction)
apiRouter.GET("/comment/list/", controller.CommentList)
// extra apis - II
apiRouter.POST("/relation/action/", controller.RelationAction)
apiRouter.GET("/relation/follow/list/", controller.FollowList)
apiRouter.GET("/relation/follower/list/", controller.FollowerList)
apiRouter.GET("/relation/friend/list/", controller.FriendList)
apiRouter.GET("/message/chat/", controller.MessageChat)
apiRouter.POST("/message/action/", controller.MessageAction)
}
顺带一提的是:static是一个保留关键字,表示静态资源。
在这个项目中,r.Static("/static", "./public")表示将/static路径映射到项目中public文件夹,所以当你访问http://localhost:8080/static/xxx时,程序会在"public"文件夹中寻找"xxx"文件并返回给客户端。
service/
message.go
对于func process
更具体地: 读取来自客户端的消息,将消息解码成 controller.MessageSendEvent 结构体, 然后根据该消息的接收方的 ID 从 chatConnMap 中查找该接收方的连接,并将消息发送给该接收方。 如果消息的内容为空,则将发送方的连接存入 chatConnMap 中。 如果不存在接收方的连接,则说明该接收方不在线。
// 初始化一个名为chatConnMap的sync.Map类型的变量
var chatConnMap = sync.Map{}
// 用于运行消息服务器,包括监听端口和接受连接请求
func RunMessageServer() {
// 监听本地9090端口的tcp连接
listen, err := net.Listen("tcp", "127.0.0.1:9090")
// 如果监听失败,输出错误信息并退出
if err != nil {
fmt.Printf("Run message sever failed: %v\n", err)
return
}
// 如果监听成功,在循环中不断接受连接请求,调用process(conn)处理连接
for {
conn, err := listen.Accept()
if err != nil {
fmt.Printf("Accept conn failed: %v\n", err)
continue
}
// 每当有新的连接请求时,就会启动一个 goroutine 调用 process() 函数处理该连接
go process(conn)
}
}
// 用于处理连接请求,包括读取消息、解析消息、发送消息
func process(conn net.Conn) {
// 在函数结束时关闭连接
defer conn.Close()
var buf [256]byte
// 在循环中读取客户端发来的数据
for {
n, err := conn.Read(buf[:])
// 如果读取失败或读取到的数据长度为0,输出错误信息并继续
if n == 0 {
if err == io.EOF {
break
}
fmt.Printf("Read message failed: %v\n", err)
continue
}
// 将读取到的数据反序列化为MessageSendEvent类型
var event = controller.MessageSendEvent{}
_ = json.Unmarshal(buf[:n], &event)
fmt.Printf("Receive Message:%+v\n", event)
fromChatKey := fmt.Sprintf("%d_%d", event.UserId, event.ToUserId)
// 如果消息内容为空,将连接存入chatConnMap中,继续
if len(event.MsgContent) == 0 {
chatConnMap.Store(fromChatKey, conn)
continue
}
// 否则,检查接收方是否在线,如果不在线,输出错误信息并继续
toChatKey := fmt.Sprintf("%d_%d", event.ToUserId, event.UserId)
writeConn, exist := chatConnMap.Load(toChatKey)
if !exist {
fmt.Printf("User %d offline\n", event.ToUserId)
continue
}
// 将消息序列化为MessagePushEvent类型,发送给接收方
pushEvent := controller.MessagePushEvent{
FromUserId: event.UserId,
MsgContent: event.MsgContent,
}
pushData, _ := json.Marshal(pushEvent)
_, err = writeConn.(net.Conn).Write(pushData)
// 如果发送失败,输出错误信息
if err != nil {
fmt.Printf("Push message failed: %v\n", err)
}
}
}
controller/
comment.go
对于func CommentAction
更具体地: CommentAction 函数接收一个 gin.Context 参数, 并从中解析出请求中的 token 和 action_type 两个参数。 然后判断这个 token 是否在 usersLoginInfo 这个 map 中存在, 如果存在,再根据 action_type 的值来决定返回不同的响应。如果 token 不存在或者 action_type 的值不是 "1",则返回一个状态码为0的响应。
type CommentListResponse struct {
Response
CommentList []Comment `json:"comment_list,omitempty"`
}
type CommentActionResponse struct {
Response
Comment Comment `json:"comment,omitempty"`
}
// CommentAction没有实际效果,只是检查token是否有效
func CommentAction(c *gin.Context) {
token := c.Query("token")
actionType := c.Query("action_type")
if user, exist := usersLoginInfo[token]; exist {
if actionType == "1" {
text := c.Query("comment_text")
c.JSON(http.StatusOK, CommentActionResponse{Response: Response{StatusCode: 0},
Comment: Comment{
Id: 1,
User: user,
Content: text,
CreateDate: "05-01",
}})
return
}
c.JSON(http.StatusOK, Response{StatusCode: 0})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
}
}
// CommentList 所有视频都有相同的演示评论列表
func CommentList(c *gin.Context) {
// 返回一个 CommentListResponse 类型的响应
c.JSON(http.StatusOK, CommentListResponse{
Response: Response{StatusCode: 0},
CommentList: DemoComments,
// CommentList 字段被赋值为 DemoComments,这个变量在其他地方被定义,
// 并且是一个数组,里面存放着类型为 Comment 的元素
})
}
common.go
type Response struct {
// 表示响应的状态码
StatusCode int32 `json:"status_code"`
// 表示响应的状态消息
StatusMsg string `json:"status_msg,omitempty"`
}
// 表示一个视频
type Video struct {
Id int64 `json:"id,omitempty"`
Author User `json:"author"`
PlayUrl string `json:"play_url" json:"play_url,omitempty"`
CoverUrl string `json:"cover_url,omitempty"`
FavoriteCount int64 `json:"favorite_count,omitempty"`
CommentCount int64 `json:"comment_count,omitempty"`
IsFavorite bool `json:"is_favorite,omitempty"`
}
// 表示一条评论
type Comment struct {
Id int64 `json:"id,omitempty"`
User User `json:"user"`
Content string `json:"content,omitempty"`
CreateDate string `json:"create_date,omitempty"`
}
// 表示一个用户
type User struct {
Id int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
FollowCount int64 `json:"follow_count,omitempty"`
FollowerCount int64 `json:"follower_count,omitempty"`
IsFollow bool `json:"is_follow,omitempty"`
}
// 表示一条信息
type Message struct {
Id int64 `json:"id,omitempty"`
Content string `json:"content,omitempty"`
CreateTime string `json:"create_time,omitempty"`
}
// 表示发送消息事件
type MessageSendEvent struct {
UserId int64 `json:"user_id,omitempty"`
ToUserId int64 `json:"to_user_id,omitempty"`
MsgContent string `json:"msg_content,omitempty"`
}
// 表示推送消息事件
type MessagePushEvent struct {
FromUserId int64 `json:"user_id,omitempty"`
MsgContent string `json:"msg_content,omitempty"`
}
demo_data.go
// 视频数组,包含一个演示视频,视频有一些属性
var DemoVideos = []Video{
{
Id: 1,
Author: DemoUser,
PlayUrl: "https://www.w3schools.com/html/movie.mp4",
CoverUrl: "https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg",
FavoriteCount: 0,
CommentCount: 0,
IsFavorite: false,
},
}
// 评论数组,包含一个演示评论,一样有一些属性
var DemoComments = []Comment{
{
Id: 1,
User: DemoUser,
Content: "Test Comment",
CreateDate: "05-01",
},
}
// 用户变量,也有一些属性
var DemoUser = User{
Id: 1,
Name: "TestUser",
FollowCount: 0,
FollowerCount: 0,
IsFollow: false,
}
// 这些演示数据可能在某些接口中被使用,作为默认数据返回给前端
favorite.go
一句话解释:这个程序定义了两个处理视频收藏夹的函数
对于func FavoriteAction
更具体地: 第一个函数 FavoriteAction 使用*gin.Context作为参数, 它是一个指向包含当前HTTP请求及其上下文信息的结构体的指针。 该函数从请求中提取一个 token 查询参数,并检查它是否存在于 usersLoginInfo 映射中。 如果是,该函数将发送一个状态码为0的JSON响应,表示成功。 如果 token 不存在,该函数将发送一个带有状态代码1和状态消息“用户不存在”的JSON响应。
// FavoriteAction没有实际效果,只是检查token是否有效
func FavoriteAction(c *gin.Context) {
token := c.Query("token")
if _, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, Response{StatusCode: 0})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
}
}
// FavoriteList 所有用户都有相同的收藏视频列表
/*
第二个功能 Favoritelist 它发送一个包含 VideoListResponse 结构体的JSON响应。
该结构体包含一个状态码为0的 Response 结构体和一个 DemoVideos 的 Video 结构体数组。
这个函数返回所有用户收藏的视频列表。
*/
func FavoriteList(c *gin.Context) {
c.JSON(http.StatusOK, VideoListResponse{
Response: Response{
StatusCode: 0,
},
VideoList: DemoVideos,
})
}
feed.go
一句话解释:这段代码提供了一个Feed接口,返回一个固定的视频列表给每个请求。
type FeedResponse struct {
Response
VideoList []Video `json:"video_list,omitempty"`
NextTime int64 `json:"next_time,omitempty"`
// VideoList字段是一个视频列表,NextTime字段是下一次请求的时间戳
}
// Feed 对于每个请求都提供相同的演示视频列表
func Feed(c *gin.Context) {
c.JSON(http.StatusOK, FeedResponse{
Response: Response{StatusCode: 0},
VideoList: DemoVideos,
NextTime: time.Now().Unix(),
})
}
message.go
一句话解释:这段代码实现了用户之间的聊天功能,包括发送消息和获取聊天记录。
对于func MessageAction
更具体地: MessageAction 函数是一个处理用户发送消息的接口, 它会接收一个 token、to_user_id 和 content 参数。 如果 token 是有效的,那么就会创建一条新的消息,并将其追加到临时消息列表中。
对于func MessageChat
更具体地: MessageChat 函数是一个获取与另一个用户的聊天记录的接口, 它会接收一个 token 和 to_user_id 参数。 如果 token 是有效的,那么就会返回与该用户的聊天记录。
var tempChat = map[string][]Message{}
var messageIdSequence = int64(1)
type ChatResponse struct {
Response
MessageList []Message `json:"message_list"`
}
// MessaaeAction没有实际的效果,只是检查token是否有效
func MessageAction(c *gin.Context) {
token := c.Query("token")
toUserId := c.Query("to_user_id")
content := c.Query("content")
if user, exist := usersLoginInfo[token]; exist {
userIdB, _ := strconv.Atoi(toUserId)
chatKey := genChatKey(user.Id, int64(userIdB))
atomic.AddInt64(&messageIdSequence, 1)
curMessage := Message{
Id: messageIdSequence,
Content: content,
CreateTime: time.Now().Format(time.Kitchen),
}
if messages, exist := tempChat[chatKey]; exist {
tempChat[chatKey] = append(messages, curMessage)
} else {
tempChat[chatKey] = []Message{curMessage}
}
c.JSON(http.StatusOK, Response{StatusCode: 0})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
}
}
// MessageChat 所有用户都有相同的关注列表
func MessageChat(c *gin.Context) {
token := c.Query("token")
toUserId := c.Query("to_user_id")
if user, exist := usersLoginInfo[token]; exist {
userIdB, _ := strconv.Atoi(toUserId)
chatKey := genChatKey(user.Id, int64(userIdB))
c.JSON(http.StatusOK, ChatResponse{Response: Response{StatusCode: 0}, MessageList: tempChat[chatKey]})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
}
}
/*
genChatKey 函数是一个生成两个用户之间聊天的键值的函数,
它会接收两个 int64 类型的参数(userIdA和userIdB)并返回一个字符串。
该字符串是由两个用户 ID 组成的,中间用下划线隔开。
*/
func genChatKey(userIdA int64, userIdB int64) string {
if userIdA > userIdB {
return fmt.Sprintf("%d_%d", userIdB, userIdA)
}
return fmt.Sprintf("%d_%d", userIdA, userIdB)
}
publish.go
一句话解释:主要实现了发布视频的功能,包括上传视频文件到服务器并存储在指定目录,还有获取所有用户发布的视频列表。
type VideoListResponse struct {
Response
VideoList []Video `json:"video_list"`
}
// Publish 检查 token 是否有效,如果有效则将上传的文件保存到public目录下,并返回成功信息
func Publish(c *gin.Context) {
token := c.PostForm("token")
if _, exist := usersLoginInfo[token]; !exist {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
return
}
data, err := c.FormFile("data")
if err != nil {
c.JSON(http.StatusOK, Response{
StatusCode: 1,
StatusMsg: err.Error(),
})
return
}
filename := filepath.Base(data.Filename)
user := usersLoginInfo[token]
finalName := fmt.Sprintf("%d_%s", user.Id, filename)
saveFile := filepath.Join("./public/", finalName)
if err := c.SaveUploadedFile(data, saveFile); err != nil {
c.JSON(http.StatusOK, Response{
StatusCode: 1,
StatusMsg: err.Error(),
})
return
}
c.JSON(http.StatusOK, Response{
StatusCode: 0,
StatusMsg: finalName + " uploaded successfully",
})
}
// PublishList 返回所有用户的发布的视频列表,在这个例子中使用的是一个示例视频列表
func PublishList(c *gin.Context) {
c.JSON(http.StatusOK, VideoListResponse{
Response: Response{
StatusCode: 0,
},
VideoList: DemoVideos,
})
}
relation.go
一句话解释:模拟用户关系相关的操作,并不是真正的实现用户关系的逻辑,只是返回了固定的模拟数据。
type UserListResponse struct {
Response
UserList []User `json:"user_list"`
}
// RelationAction 检查token是否有效,如果存在就返回一个状态码为0的响应,
// 否则返回状态码为1并且状态信息为 "User doesn't exist" 的响应
func RelationAction(c *gin.Context) {
token := c.Query("token")
if _, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, Response{StatusCode: 0})
} else {
c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
}
}
// FollowList 返回所有用户的关注列表,数据都是固定的,即只有一个 DemoUser
func FollowList(c *gin.Context) {
c.JSON(http.StatusOK, UserListResponse{
Response: Response{
StatusCode: 0,
},
UserList: []User{DemoUser},
})
}
// FollowerList 返回所有用户的粉丝列表,数据都是固定的,即只有一个 DemoUser
func FollowerList(c *gin.Context) {
c.JSON(http.StatusOK, UserListResponse{
Response: Response{
StatusCode: 0,
},
UserList: []User{DemoUser},
})
}
// FriendList 返回所有用户的好友列表,同样,数据都是固定的,即只有一个 DemoUser
func FriendList(c *gin.Context) {
c.JSON(http.StatusOK, UserListResponse{
Response: Response{
StatusCode: 0,
},
UserList: []User{DemoUser},
})
}
user.go
一句话解释:实现了用户相关的功能,包括用户注册、登录、查询用户信息等。
type UserLoginResponse struct {
Response
UserId int64 `json:"user_id,omitempty"`
Token string `json:"token"`
}
type UserResponse struct {
Response
User User `json:"user"`
}
// Register函数用于实现用户注册功能,会接受用户名和密码作为参数,并检查是否已经存在该用户,
// 如果不存在则新建用户,并返回注册成功信息。
func Register(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
token := username + password
if _, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, UserLoginResponse{
Response: Response{StatusCode: 1, StatusMsg: "User already exist"},
})
} else {
atomic.AddInt64(&userIdSequence, 1)
newUser := User{
Id: userIdSequence,
Name: username,
}
usersLoginInfo[token] = newUser
c.JSON(http.StatusOK, UserLoginResponse{
Response: Response{StatusCode: 0},
UserId: userIdSequence,
Token: username + password,
})
}
}
// Login函数用于实现用户登录功能,会接受用户名和密码作为参数,并检查是否已经存在该用户,
// 如果存在则返回登录成功信息。
func Login(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
token := username + password
if user, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, UserLoginResponse{
Response: Response{StatusCode: 0},
UserId: user.Id,
Token: token,
})
} else {
c.JSON(http.StatusOK, UserLoginResponse{
Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"},
})
}
}
// UserInfo函数用于查询用户的信息,会接受token作为参数,并检查是否存在该用户,
// 如果存在则返回用户信息。
func UserInfo(c *gin.Context) {
token := c.Query("token")
if user, exist := usersLoginInfo[token]; exist {
c.JSON(http.StatusOK, UserResponse{
Response: Response{StatusCode: 0},
User: user,
})
} else {
c.JSON(http.StatusOK, UserResponse{
Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"},
})
}
}
test/
单元测试部分的代码暂时不进行解释,不过后续很快更新出来,敬请期待...
未完待续...(随时补充修改)
谢谢大家的阅读,欢迎互动,也欢迎访问我的博客!