TikTok-Demo详解 | 青训营笔记

500 阅读12分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 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/

单元测试部分的代码暂时不进行解释,不过后续很快更新出来,敬请期待...


未完待续...(随时补充修改)

谢谢大家的阅读,欢迎互动,也欢迎访问我的博客!

conqueror712.github.io/