尚硅谷go语言聊天室项目

1.需求分析

(1)用户登录

(2)用户注册

(3)用户列表

(4)群聊

(5)点对点聊天

(6)离线留言

2.功能实现

2.1用户登录和注册

2.1.1客户端

系统主界面,包含登录、注册、退出功能,登陆注册需要用户的ID和密码。调用处理层方法实现功能。

package main

import (
	"fmt"
	"main/QQ/client/process"
)

var userId int
var userPwd string
var userName string

func main() {
	var key int
	var loop bool = true
	for loop {
		fmt.Println("-----欢迎登陆多人聊天系统-----")
		fmt.Println("\t\t1.登录")
		fmt.Println("\t\t2.注册")
		fmt.Println("\t\t3.退出")
		fmt.Print("请输入选项:")
		fmt.Scanln(&key)
		switch key {
		case 1:
			fmt.Println("登录系统")
			fmt.Print("请输入用户Id:")
			fmt.Scanln(&userId)
			fmt.Print("请输入密码:")
			fmt.Scanln(&userPwd)
			up := &process.UserProcess{}
			up.Login(userId, userPwd)
		case 2:
			fmt.Println("注册账号")
			fmt.Print("请输入用户Id:")
			fmt.Scanln(&userId)
			fmt.Print("请输入密码:")
			fmt.Scanln(&userPwd)
			fmt.Print("请输入用户名:")
			fmt.Scanln(&userName)
			up := &process.UserProcess{}
			up.Register(userId, userPwd, userName)
		case 3:
			fmt.Println("退出系统")
			loop = false
		default:
			fmt.Println("请重新输入")
		}
	}

}

process包中包含各种业务处理,创建userProcess包用于处理用户注册、管理;创建结构体作为接收者。用户向服务器发送连接信息需要序列化。

创建message包,里面包含各种需要序列化的结构体,Message用于在用户端和服务端传送,包含了待发送的消息类型和消息数据,其中消息数据要先转为字符串。

package message
const (
	LoginMesType            = "LoginMes"
	LoginResMesType         = "LoginResMes"
)
type Message struct {
	Type string `json:"type"`
	Data string `json:"data"`
}
type LoginMes struct {
	UserId   int    `json:"userId"`
	UserPwd  string `json:"userPwd"`
	UserName string `json:"userName"`
}
 在发送时为了增加通信时的健壮性,这里采用先发送长度再发送主体数据。因为发送所需要的操作都是一样的,这里将其封装到utils包中,同理接收也先接受长度再接收数据。
其他包通过对象调取方法,在发送或接收时需要知道建立的连接以及发送的数据
type Transfer struct {
	Conn net.Conn
	Buf  [8096]byte
}
func (this *Transfer) WritePkg(data []byte) (err error) {
	//发送data 为了防止发送错误,先发送长度
	var pkgLen uint32
	pkgLen = uint32(len(data))
	binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
	n, err := this.Conn.Write(this.Buf[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn,write err", err)
		return
	}
	fmt.Printf("发送长度=%d,发送内容=%v", len(data), string(data))
	//传送数据
	_, err = this.Conn.Write(data)
	if err != nil {
		fmt.Println("conn write err=", err)
		return
	}
	return
}
func (this *Transfer) ReadPkg() (mes message.Message, err error) {
	fmt.Println("读取客户端发送消息")
	_, err = this.Conn.Read(this.Buf[:4])
	if err != nil {
		fmt.Println("conn read err1=", err)
		return
	}
	var pkgLen uint32
	pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
	n, err := this.Conn.Read(this.Buf[:pkgLen])
	if n != int(pkgLen) || err != nil {
		//fmt.Println("conn read err3=", err)
		return
	}
	//反序列化
	err = json.Unmarshal(this.Buf[:pkgLen], &mes)
	if err != nil {
		fmt.Println("Server unmarshal err=", err)
	}
	return
}

用户登陆时,向服务器发送的消息类型为登陆类型。将id和pwd封装成message.LoginMes,序列化成json字符数组,转换为mes的data后序列化mes发送。

发送登录消息至服务器后若返回为登陆成功,则服务端应该将建立的连接维护起来 ,即创建一个结构体,其中包含了用户的属性信息以及本次连接信息

type CurUser struct {
	message.User
	Conn net.Conn
}
// Login 用户登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	//连接服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()
	//创建消息mes
	var mes message.Message
	mes.Type = message.LoginMesType
	//创建login消息并序列化
	var loginMes message.LoginMes
	loginMes.UserId = userId
	loginMes.UserPwd = userPwd
	data, err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("login marshal err=", err)
		return
	}
	//data赋值给mes
	mes.Data = string(data)
	//mes序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("mes err=", err)
		return
	}
	tf := &utils.Transfer{
		Conn: conn,
	}
	tf.WritePkg(data)
	// 客户端接受返回消息
	resMes, err := tf.ReadPkg()
	if err != nil {
		fmt.Println("read err=", err)
		return
	}
	var loginRes message.LoginResMes
	err = json.Unmarshal([]byte(resMes.Data), &loginRes)
	if err != nil {
		fmt.Println("unmarshal err=", err)
	}
	//返回登陆成功
	if loginRes.Code == 200 {
		CurUser.Conn = conn
		CurUser.UserId = userId
		for _, v := range loginRes.UserIds {
			if v == userId {
				continue
			}
			fmt.Println("用户id:", v)
		}
		go ServerProcessMes(conn)
		for {
			//登陆界面
			ShowMenu()
			//与服务器建立连接
		}
	} else {
		fmt.Println(loginRes.Error)
	}
	return
}

2.1.2 服务端

在服务端需要不断监听客户端发送信息,同时还要根据客户端发送的信息对数据库实行对应的操作。本项目采用redis数据库,在开启服务器时初始化线程池,线程池被封装到UserDao中,对数据库操作需要先通过userDao创建redis连接,服务端只需要一个userDao就可以为所有客户分配数据库连接。

var (
	MyUserDao *UserDao
)

type UserDao struct {
	pool *redis.Pool
}

// NewUserDao 使用工厂创建userDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
	return &UserDao{
		pool: pool,
	}
}
var (
	pool *redis.Pool
)

func InitPool(address string, maxIdle, maxActive int, idleTimeOut time.Duration) {
	pool = &redis.Pool{
		MaxIdle:     maxIdle,
		MaxActive:   maxActive,
		IdleTimeout: idleTimeOut,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", address)
		},
	}
}

 客户端在与服务端连接上后就要开启一个协程,以便主线程还可以处理其他客户请求信息,相较于客户端,服务端的连接会有多条,所以服务端的服务层在创建结构体时必须要写入连接。

func main() {
	InitPool("localhost:6379", 16, 0, 300*time.Second)
	initUserDao()
	fmt.Println("服务器在8889端口接听")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	defer listen.Close()
	if err != nil {
		fmt.Println("dial err=", err)
		return
	}
	for {
		time.Sleep(5 * time.Second)
		fmt.Println("等待客户端连接")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept err=", err)
		}
		go process(conn)
	}
}

func initUserDao() {
	model.MyUserDao = model.NewUserDao(pool)
}

func process(conn net.Conn) {
	defer conn.Close()
	processor := &Processor{
		Conn: conn,
	}
	err := processor.process1()
	if err != nil {
		fmt.Println("开启协程错误")
		return
	}
}
type Processor struct {
	Conn net.Conn
}

服务器接收消息 

func (this *Processor) process1() (err error) {
	for {
		tf := &utils.Transfer{
			Conn: this.Conn,
		}
		//读取客户端发送数据包
		mes, err := tf.ReadPkg()
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端退出,服务端退出")
				return err
			} else {
				fmt.Println("readPkg err=", err)
				return err
			}
		}
		err = this.ServerProcessMes(&mes)
		if err != nil {
			return err
		}
	}
}

客户端在接收到mes信息时,需要判断信息的种类进而进行不同的操作。 

func (this *Processor) ServerProcessMes(mes *message.Message) (err error) {
	switch mes.Type {
	case message.LoginMesType:
		//用户登录
		up := &process1.UserProcess{
			Conn: this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	case message.RegisterMesType:
		// 用户注册
		up := &process1.UserProcess{
			Conn: this.Conn,
		}
		err = up.ServiceProcessRegister(mes)
	case message.SmsMesType:
		sp := &process1.SmsProcess{}
		sp.SendGropeMes(mes)
	default:
		fmt.Println("未知类型,无法处理")
	}
	return
}

当为登录和注册是分别在process包中调用对应的函数

func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
	// string类型登录信息
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data), &loginMes)
	if err != nil {
		fmt.Println("unmarshal fail err=", err)
	}
	// 返回消息
	var resMes message.Message
	resMes.Type = message.LoginResMesType
	// 返回消息数据
	var loginResMes message.LoginResMes
	user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)
	if err != nil {
		if err == model.ERROR_USER_NOTEXISTS {
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD {
			loginResMes.Code = 403
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 505
			loginResMes.Error = "未知错误"
		}
	} else {
		loginResMes.Code = 200
		this.UserId = loginMes.UserId
		userMgr.AddOnlineUser(this)
		this.NotifyOthersOnlineUser(this.UserId)
		for id, _ := range userMgr.onlineUsers {
			loginResMes.UserIds = append(loginResMes.UserIds, id)
		}
		fmt.Println(user, "登录成功")

	}

	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("marshal err= ", err)
		return
	}
	resMes.Data = string(data)
	// 将resMes序列化后发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("marshal err=", err)
	}
	tf := &utils.Transfer{
		Conn: this.Conn,
	}
	err = tf.WritePkg(data)
	return
}

// ServiceProcessRegister 注册
func (up *UserProcess) ServiceProcessRegister(mes *message.Message) (err error) {
	var registerMes message.RegisterMes
	err = json.Unmarshal([]byte(mes.Data), &registerMes)
	if err != nil {
		fmt.Println("registerMes unmarshal err=", err)
		return
	}
	var resMes message.Message
	resMes.Type = message.RegisterResMesType
	var registerResMes message.RegisterResMes
	err = model.MyUserDao.Register(&registerMes.User)
	if err != nil {
		if err == model.ERROR_USER_EXISTS {
			registerResMes.Code = 505
			registerResMes.Error = model.ERROR_USER_EXISTS.Error()
		} else {
			registerResMes.Code = 506
			registerResMes.Error = "注册时未知错误"
		}
	} else {
		registerResMes.Code = 200
	}
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("registerResMes Marshal err=", err)
		return
	}
	resMes.Data = string(data)
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("register resMes Marshal err=", err)
		return
	}
	tf := &utils.Transfer{Conn: up.Conn}
	err = tf.WritePkg(data)
	return
}

登录和注册都会对redis数据库进行操作,前面已经初始过线程池并封装到userDao中,通过userDao获取数据库连接并对其进行增删改查操作。

func (userDao *UserDao) Login(userId int, userPwd string) (user *message.User, err error) {
	conn := userDao.pool.Get()
	defer conn.Close()
	user, err = userDao.getUserById(conn, userId)
	if err != nil {
		return
	}
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}
func (userDao *UserDao) Register(user *message.User) (err error) {
	conn := userDao.pool.Get()
	defer conn.Close()
	_, err = userDao.getUserById(conn, user.UserId)
	if err == nil {
		err = ERROR_USER_EXISTS
		return
	}
	// 操作redis完成注册
	data, err := json.Marshal(user)
	_, err = conn.Do("HSet", "users", user.UserId, string(data))
	if err != nil {
		fmt.Println("保存时注册失败")
		return
	}
	return
}
func (userDao *UserDao) getUserById(conn redis.Conn, id int) (user *message.User, err error) {
	res, err := redis.String(conn.Do("HGet", "users", id))
	if err != nil {
		if err == redis.ErrNil {
			err = ERROR_USER_NOTEXISTS
		}
		return
	}
	user = &message.User{}
	err = json.Unmarshal([]byte(res), user)
	if err != nil {
		fmt.Println("Unmarshal err=", err)
		return
	}
	return
}

2.2用户列表

2.2.1服务端

在用户登陆成功后,需要能够显示已经登陆过的成员列表。在服务器端维护一个在线成员列表,结构为map[id]*UserProcess,当成员登陆时,将成员信息写入map,并将map的id写入数组当作登陆返回信息返回给用户。

var (
	userMgr *UserMgr
)

type UserMgr struct {
	onlineUsers map[int]*UserProcess
}

func init() {
	userMgr = &UserMgr{
		onlineUsers: make(map[int]*UserProcess, 1024),
	}
}
func (mgr *UserMgr) AddOnlineUser(up *UserProcess) {
	mgr.onlineUsers[up.UserId] = up
}
func (mgr *UserMgr) DeleteOnlineUser(userId int) {
	delete(mgr.onlineUsers, userId)
}
func (mgr *UserMgr) getAllOnlineUser() map[int]*UserProcess {
	return mgr.onlineUsers
}
func (mgr *UserMgr) getOnlineUserById(userId int) (up *UserProcess, err error) {
	up, ok := mgr.onlineUsers[userId]
	if !ok { //用户不在线
		err = fmt.Errorf("用户%d不在线", userId)
		return
	}
	return
}
type UserProcess struct {
	Conn   net.Conn
	UserId int
}
userMgr.AddOnlineUser(this)
this.NotifyOthersOnlineUser(this.UserId)
for id, _ := range userMgr.onlineUsers {
			loginResMes.UserIds = append(loginResMes.UserIds, id)
		}

2.2.2客户端

客户端在本地会维护一个map记录用户的登陆状态,这样就不用每次查找时都访问服务器了,每次用户的登录信息改变时,服务器会向所有客户端发送消息更改本地信息。

// 客户端维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
func outputOnlineUser() {
	fmt.Println("显示用户列表")
	for id, user := range onlineUsers {
		fmt.Printf("id=%d,userName=%s\n", id, user.UserName)
	}
}

// 更新用户登录信息(更新map)
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
	user, ok := onlineUsers[notifyUserStatusMes.UserId]
	if !ok {
		user = &message.User{
			UserId:     notifyUserStatusMes.UserId,
			UserStatus: notifyUserStatusMes.Status,
		}
	}
	user.UserStatus = notifyUserStatusMes.Status
	onlineUsers[user.UserId] = user
}
for _, v := range loginRes.UserIds {
			if v == userId {
				continue
			}
			user := &message.User{
				UserId:     v,
				UserStatus: message.UserOnline,
			}
			onlineUsers[v] = user
		}

 2.3 群聊

2.3.1 客户端

菜单调用

case 2:
		fmt.Println("send message")
		fmt.Println("context:")
		fmt.Scanln(&context)
		sp.SendGropeMes(context)

通过保存的连接向服务器发送消息 

func (this *SmsProcess) SendGropeMes(context string) (err error) {
	var mes message.Message
	mes.Type = message.SmsMesType

	var smsMes message.SmsMes
	smsMes.UserId = CurUser.UserId
	smsMes.Content = context

	data, err := json.Marshal(smsMes)
	if err != nil {
		fmt.Println("smsMes Marshal err=", err)
		return
	}
	mes.Data = string(data)

	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("data marshal err=", err)
		return
	}
	tf := &utils.Transfer{Conn: CurUser.Conn}
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("sendGropeMes err=", err)
		return
	}
	return
}

 其他客户端接收消息

case message.SmsMesType:
			outputGropeMes(&mes)
func outputGropeMes(mes *message.Message) {
	var smsMes message.SmsMes
	err := json.Unmarshal([]byte(mes.Data), &smsMes)
	if err != nil {
		fmt.Println("outputGropeMes err=", err)
		return
	}
	info := fmt.Sprintf("%d对你说:%s", smsMes.UserId, smsMes.Content)
	fmt.Println(info)

}

 2.3.2 服务端

服务器端接收来自客户端的数据报

case message.SmsMesType:
		sp := &process1.SmsProcess{}
		sp.SendGropeMes(mes)

 向其他客户端发送

type SmsProcess struct {
}
func (sp *SmsProcess) SendGropeMes(mes *message.Message) {
	var smsMes message.SmsMes
	err := json.Unmarshal([]byte(mes.Data), &smsMes)
	if err != nil {
		fmt.Println("SendGropeMes err", err)
		return
	}
	data, err := json.Marshal(mes)
	if err != nil {
		fmt.Println("sendGropeMes Marshal err=", err)
		return
	}
	for id, up := range userMgr.onlineUsers {
		if id == smsMes.UserId {
			continue
		}
		sp.SendMesToOthers(data, up.Conn)
	}
}

func (sp *SmsProcess) SendMesToOthers(data []byte, conn net.Conn) {
	tf := utils.Transfer{
		Conn: conn,
	}
	err := tf.WritePkg(data)
	if err != nil {
		fmt.Println("sendMesToOthers err=", err)
		return
	}
}

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值