有一段时间没写博客了,现在因为工作需要学习了go,做一个小Demo,简单的聊天室。
- 首先服务端开启监听8889端口,监听每一个客户端。
- 客户端需要先登录,登录成功后,服务端会发送给每一个客户端一个消息,XXX用户登陆成功,目前程序还没写完全,后续会不断更新~~
- message.go:
首先需要一个消息传送的protocol,因为是基于TCP协议,传输过程中会有粘包和拆包问题,因此定义一个协议来保证数据完整性。
package messageconst (LoginMesType = "LoginMes"RegisterMesType = "RegisterMesType"MesType = "MesType")type Message struct {Type string `json:"type"` // 消息类型Data string `json:"data"` // 消息内容}type LoginMes struct {UserId int `json:"userId"` // 用户idUserPwd string `json:"userPwd"` // 用户密码UserName string `json:"userName"` // 用户名}
- coding.go:
这个包是用来编码和解码的作用,当发送数据时需要分为2段编码,然后转为字节进行发送。接收数据时,通过反序列化直接就可以获得数据。
package codingimport ("bufio""bytes""encoding/binary""encoding/json""fmt")func Encode(i interface{}) (b []byte, err error) {//将message进行序列化data, err := json.Marshal(i)if err != nil {fmt.Println("json Marshal error")return}//这个时候data就是要发送的消息var pkgLen uint32var pkg = new(bytes.Buffer)pkgLen = uint32(len(data))// 写入头err = binary.Write(pkg, binary.LittleEndian, pkgLen)// 写入体err = binary.Write(pkg, binary.LittleEndian, data)b = pkg.Bytes()return}func Decode(reader *bufio.Reader) (b []byte, err error) {// 读取消息preBytes, err := reader.Peek(4)var length uint32err = binary.Read(bytes.NewBuffer(preBytes), binary.LittleEndian, &length)if err != nil {fmt.Println("读取头4字节失败", err)return}if uint32(reader.Buffered()) < length+4 {return []byte{}, fmt.Errorf("读取到%d,应为%d", reader.Buffered(), length+4)}fmt.Println("body长度为", length)p := make([]byte, length+4)_, err = reader.Read(p)if err != nil {fmt.Println("读取头数据体失败")return}b = p[4:]return}
- server.go
服务器主要代码,因为是个简单demo,没有考虑很多。
package mainimport ("bufio""coding""encoding/json""fmt""message""net""redisCli""strconv")var r *redisCli.RedisClivar online map[net.Conn]message.LoginMesfunc init() {r, _ = redisCli.NewRedisCli("tcp", "127.0.0.1:6379")online = make(map[net.Conn]message.LoginMes, 10)}func handleLogin(b []byte) (loginMes message.LoginMes) {_ = json.Unmarshal(b, &loginMes)//使用redis操作是否存在该账号str, err := r.GetString(strconv.Itoa(loginMes.UserId) + ":" + loginMes.UserPwd)if err != nil {return}//if str == "" {// fmt.Println("登录失败")// return//}loginMes.UserName = strreturn}func handleRegister(b []byte) {}func process(conn net.Conn) {// 读取客户端发送的信息reader := bufio.NewReader(conn)mes := message.Message{}b, err := coding.Decode(reader)if err != nil {fmt.Println("decode失败")return}err = json.Unmarshal(b, &mes)if err != nil {fmt.Println("反序列化失败")return}dataBytes := []byte(mes.Data)switch mes.Type {case message.LoginMesType: // 登录消息loginMes := handleLogin(dataBytes)online[conn] = loginMesmes := message.Message{Type: message.MesType,Data: loginMes.UserName + "加入群聊啦",}b, err := coding.Encode(mes)if err != nil {fmt.Println("序列化失败")return}fo20000r c, _ := range online {_, _ = c.Write(b)}case message.RegisterMesType: // 注册消息handleRegister(dataBytes)}}func main() {fmt.Println("服务器从8889端口监听")listen, err := net.Listen("tcp", "0.0.0.0:8889")if err != nil {fmt.Println("net listen error")return}//监听成功等待for {conn, err := listen.Accept()if err != nil {fmt.Println("net client error")return}//连接成功就保持通讯go process(conn)}}
- client.go:
客户端代码
package mainimport ("bufio""coding""context""fmt""message""sync")var (userId intuserPwd string)var wg sync.WaitGroupfunc main() {// 接收用户的选择var key int// 判断是否还继续显示菜单var loop = truefor loop {fmt.Println("----------欢迎登录多人聊天系统----------")fmt.Println("\\t\\t\\t 1 登录聊天系统")fmt.Println("\\t\\t\\t 2 注册用户")fmt.Println("\\t\\t\\t 3 退出系统")fmt.Println("\\t\\t\\t 请选择(1-3):")_, _ = fmt.Scanf("%d\\n", &key)switch key {case 1:fmt.Println("登录聊天系统")loop = falsecase 2:fmt.Println("注册用户")case 3:fmt.Println("退出系统")loop = falsedefault:fmt.Println("输入有误 请重新输入")}}if key == 1 {// 说明用户要登录fmt.Println("请输入用户id")_, _ = fmt.Scanf("%d\\n", &userId)fmt.Println("请输入用户密码")_, _ = fmt.Scanf("%s\\n", &userPwd)// 先把登录的函数写到另一个文件中conn, err := login(userId, userPwd)if err != nil {fmt.Println("登录失败了")} else {fmt.Println("登录成功了")// 这里需要监听服务器发来的信息ctx, cancel := context.WithCancel(context.Background())wg.Add(2)// 开启接收端进程go func(ctx context.Context, cancel context.CancelFunc) {fmt.Println("开启接收进程")defer wg.Done()defer cancel()for {select {case <-ctx.Done():fmt.Println("退出接收进程")returndefault:b, err := coding.Decode(bufio.NewReader(conn))if err != nil {fmt.Println("退出接收进程, err:", err)return}str := string(b)fmt.Println("收到消息:", str)if "exit" == str {return}}}}(ctx, cancel)// 开启发送端进程go func(ctx context.Context) {defer wg.Done()fmt.Println("开启写入进程")for {select {case <-ctx.Done():fmt.Println("退出写入进程")returndefault:var str string_, _ = fmt.Scanf("%s\\n", &str)mes := message.Message{Type: message.MesType,Data: str,}b, err := coding.Encode(mes)if err != nil {fmt.Println("序列化失败")continue}_, _ = conn.Write(b)}}}(ctx)wg.Wait()}} else if key == 2 {fmt.Println("进行用户注册")}}// 写一个函数,完成登录func login(userId int, userPwd string) (conn net.Conn, err error) {// 开始定协议conn, err = net.Dial("tcp", "localhost:8889")if err != nil {fmt.Println("net Dial error")return}// 准备conn发送消息var mes message.Messagemes.Type = message.LoginMesType// 创建一个LoginMes 结构体var loginMes message.LoginMesloginMes.UserId = userIdloginMes.UserPwd = userPwdb, err := json.Marshal(loginMes)if err != nil {fmt.Println("json Marshal error")return}mes.Data = string(b)data, err := coding.Encode(mes)if err != nil {fmt.Println("data Marshal error")return}_, _ = conn.Write(data)return}
效果:
- 启动服务器
- 加入第一个客户端,第一个客户端显示如下
- 加入第二个客户端,第一个客户端显示如下