Go第 18 章 :tcp编程 (勤复习)

Go第 18 章 :tcp编程

18.1 看两个实际应用

QQ,迅雷,百度网盘客户端.
新浪网站,京东商城,淘宝…
请添加图片描述

18.2 网络编程基本介绍

Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少
也是至关重要的一部分。
网络编程有两种:
1) TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协 议的. 比如: QQ 聊天 [示意图]
2) b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依 旧是用 tcp socket 实现的。[示意图] 比如: 京东商城 【这属于 go web 开发范畴 】

18.2.1 网线,网卡,无线网卡

计算机间要相互通讯,必须要求网线,网卡,或者是无线网卡.
请添加图片描述

18.2.2 协议(tcp/ip)

TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互 联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单 地说,就是由网络层的 IP 协议和传输层的 TCP协议组成的。
请添加图片描述

18.2.3 OSI 与 Tcp/ip 参考模型 (推荐 tcp/ip 协议 3 卷)

请添加图片描述
请添加图片描述

18.2.4 ip 地址

概述:每个 internet 上的主机和路由器都有一个 ip地址,它包括网络号和主机号,ip地址有 ipv4(32
位)或者 ipv6(128 位). 可以通过 ipconfig 来查看
请添加图片描述

18.2.5 端口(port)-介绍

我们这里所指的端口不是指物理意义上的端口,而是特指 TCP/IP 协议中的端口,是
逻辑意义上的端口。
如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个 门,但是一个 IP 地址的端口 可以有 65536(即:256×256)个之多!端口是通过端 口号来标记的,端口号只有整数,范围是从 0 到 65535(256×256-1)
请添加图片描述

18.2.6 端口(port)-分类

请添加图片描述

18.2.7 端口(port)-使用注意
  1. 在计算机(尤其是做服务器)要尽可能的少开端口
  2. 一个端口只能被一个程序监听
  3. 如果使用 netstat –an 可以查看本机有哪些端口在监听
  4. 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口

18.3 tcp socket 编程的客户端和服务器端

请添加图片描述

18.4 tcp socket 编程的快速入门

18.4.1 服务端的处理流程
  1. 监听端口 8888
  2. 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
  3. 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
18.4.2 客户端的处理流程
  1. 建立与服务端的链接
  2. 发送请求数据[终端],接收服务器端返回的结果数据
  3. 关闭链接
18.4.3 简单的程序示意图

请添加图片描述

18.4.4 代码的实现

请添加图片描述

package main

import (
	"fmt"
	"net"
)

func process(conn net.Conn) {

	//这里我们循环的接收客户端发送的数据
	defer conn.Close() //关闭conn
	for {
		//创建一个新的切片
		buf := make([]byte, 1024)
		//conn.Read(buf)
		//1、等待客户端通过conn发送信息
		//2、如果客户端没有write【发送】,那么协程就阻塞在这里
		fmt.Printf("服务器在等待客户端%d发送信息 \n", conn.RemoteAddr().String())
		n, err := conn.Read(buf) //从conn读取
		if err != nil {
			fmt.Println("客户端退出err=", err)
			return //!!!
		}
		//3. 显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n]))
	}

}

func main() {
	fmt.Println("服务器开始监听...")
	//net.Listen("tcp","0.0.0.0:8888")
	//1. tcp 表示使用网络协议是 tcp
	//2. 0.0.0.0:8888 表示在本地监听 8888 端口
	listen, err := net.Listen("tcp", ":8888")
	if err != nil {
		fmt.Println("listen err= ", err)
		return
	}
	defer listen.Close()

	for { //循环等待客户端来连接我
		fmt.Println("等待客户端连接。。。")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("conn err=", err)
		} else {
			fmt.Printf("accept() success con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
		}
		//这里准备其一个协程,为客户端服务
		go process(conn)
	}
	//fmt.Printf("listen suc=%v\n", listen)
}

客户端功能:

  1. 编写一个客户端端程序,能链接到 服务器端的 8888 端口
    1. 客户端可以发送单行数据,然后就退出
    1. 能通过终端输入数据(输入一行发送一行), 并发送给服务器端 []
    1. 在终端输入 exit,表示退出程序.
    1. 代码:
package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main(){
	conn,err:=net.Dial("tcp",":8888")
	if err!=nil{
		fmt.Println("Dial err = ",err)
		return
	}
	//功能一:客户端可以发送单行数据,然后就退出
	reader:=bufio.NewReader(os.Stdin)//os.Stdin 代表标准输入[终端]
	//从终端读取一行用户输入,并准备发送给服务器
	line,err:=reader.ReadString('\n')
	if err!=nil{
		fmt.Println("ReadString err = ",err)
	}
	//再将 line 发送给 服务器
	n,err:=conn.Write([]byte(line))
	if err!=nil{
		fmt.Println("Write err = ",err)
	}
	fmt.Printf("客户端发送了 %d 字节的数据,并退出", n)
}

请添加图片描述
对 client.go 做了改进:
请添加图片描述

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	conn, err := net.Dial("tcp", ":8888")
	if err != nil {
		fmt.Println("Dial err = ", err)
		return
	}
	//功能一:客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
	for {
		//从终端读取一行用户输入,并准备发送给服务器
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("ReadString err = ", err)
		}
		line = strings.Trim(line, " \r\n")
		if line == "exit" {
			fmt.Println("客户端退出..")
			break
		}

		//再将 line 发送给 服务器
		_, err = conn.Write([]byte(line + "\n"))
		if err != nil {
			fmt.Println("Write err = ", err)
		}
		//fmt.Printf("客户端发送了 %d 字节的数据,并退出", n)

	}
}

18.5 经典项目-海量用户即时通讯系统

18.5.1 项目开发流程

需求分析–> 设计阶段—> 编码实现 --> 测试阶段–>实施

18.5.2 需求分析
  1. 用户注册
  2. 用户登录
  3. 显示在线用户列表
  4. 群聊(广播)
  5. 点对点聊天
  6. 离线留言
18.5.3 界面设计

请添加图片描述

18.5.4 项目开发前技术准备

项目要保存用户信息和消息数据,因此我们需要学习数据库(Redis 或者 Mysql) , 这里我们选择 Redis , 所以先给同学们讲解如何在 Golang 中使用 Redis.
请添加图片描述

18.5.5 实现功能-显示客户端登录菜单

功能:能够正确的显示客户端的菜单。
界面:
请添加图片描述
请添加图片描述
client/main.go
请添加图片描述
请添加图片描述

请添加图片描述

client/login.go
请添加图片描述

18.5.6 实现功能-完成用户登录

要求:先完成指定用户的验证,用户 id=100, 密码 pwd=123456 可以登录,其它用户不能登录
这里需要先说明一个 Message 的组成(示意图),并发送一个 Message 的流程
请添加图片描述

1.完成客户端可以发送消息长度,服务器端可以正常收到该长度值

分析思路
(1) 先确定消息 Message 的格式和结构
(2) 然后根据上图的分析完成代码
(3) 示意图
请添加图片描述
代码实现:
server/main.go

package main

import (
	"fmt"
	"net"
)

//处理和客户端的通讯
func process(conn net.Conn){
	//这里需要延时关闭conn,否则后边出现非常奇怪的现象,代码多了不好调
	defer conn.Close()
	//循环的读取客户端发送的信息
	for{
		buf := make([]byte,8096)
		fmt.Println()
		fmt.Println("开始读取客户端发送的数据......")
		n,err := conn.Read(buf[:4])
		if n!=4 || err != nil {
			fmt.Println("conn.Read,err=", err)
			return //或者干别的事情,如果不设置就会阻塞在这个地方
		}
		fmt.Printf("读到的长度为=%d, 读到的buf=%v\n",n,buf[:4])
	}
}




func main() {

	//提示信息
	fmt.Println("服务器再8889端口监听......")
	Listen, err := net.Listen("tcp", "127.0.0.1:8889")
	defer Listen.Close()
	if err != nil {
		fmt.Println("net.listen err=", err)
		return
	}
	//一旦监听成功,就等待客户端来链接服务器
	for{
		fmt.Println("等待客户端来链接服务器......")
		conn,err := Listen.Accept()
		if err != nil {
			fmt.Println("conn.Accept err=", err)
		}
		//一旦链接成功,则启动一个协程和客户端保持数据的通讯......
		go process(conn)
	}
}

common/message/message.go

package message

const(
	LoginMesType = "LoginMes"
	LoginResMesType = "LoginResMes"
)
//因为在传递序列化的时候需要变量是小写的,因此需要打一个小写的tag
type Message struct{
	Type string `json:"type"`//消息类型
	Data string `json:"data"`//消息内容
}


//定义两个消息。。。后面需要再增加
type LoginMes struct{
	UserId int `json:"userId"`//用户id
	UserPwd string `json:"userPwd"` //用户密码
	UserName string `json:"userName"`//用户名
}


type LoginResMes struct{
	Code int `json:"code"`//返回状态码 500 表示该用户未注册 200表示登录成功 300表示网络不通
	Error string `json:"error"`//错误描述 若没有错误则返回nil
}


client/main.go 和前面的代码一样,没有修改
client/login.go

package main

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"go_code/go_code/chapter18/chatroom/common/message"
	"net"
)

//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd  string) (err error){

	//下一步就要开始定协议(大事)
	//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)

	//return nil
	//1、链接服务器
	conn,err := net.Dial("tcp","localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()
	//2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType
	//3、创建一个LoginMesTypMes结构体
	var loginMes message.LoginMes
	loginMes.UserId = UserId
	loginMes.UserPwd = UserPwd

	//4、将loginMes序列化
	data,err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("loginMes json.Marshal err=", err)
		return
	}
	//5、把data赋给来mes.Data字段
	mes.Data = string(data)

	//6、将mes进行序列化
	data,err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" mes json.Marshal err=", err)
		return
	}

	//7、到这个时候,data就是我们要发送的消息

	//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
	//先获取到data的长度-》转成一个表示长度的byte切片
	// 错误写法: conn.Write(len(data))
	//正确写法:将长度转成一个bytes
	//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte //定义一个数组
	binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32

	//发送长度
	n,err := conn.Write(buf[:4])
	if n!= 4|| err!=nil{
		fmt.Println("conn.Write(bytes)fail,err=",err)
		return
	}
	fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))
	return
}
2.完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes

思路分析:
(1) 让客户端发送消息本身
(2) 服务器端接受到消息, 然后反序列化成对应的消息结构体.
(3) 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
(4) 客户端解析返回的 LoginResMes,显示对应界面
(5) 这里我们需要做函数的封装
代码实现:
client/login.go 做了修改

package main

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"go_code/go_code/chapter18/chatroom/common/message"
	"net"
	"time"
)

//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd  string) (err error){

	//下一步就要开始定协议(大事)
	//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)

	//return nil
	//1、链接服务器
	conn,err := net.Dial("tcp","localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()
	//2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType
	//3、创建一个LoginMesTypMes结构体
	var loginMes message.LoginMes
	loginMes.UserId = UserId
	loginMes.UserPwd = UserPwd

	//4、将loginMes序列化
	data,err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("loginMes json.Marshal err=", err)
		return
	}
	//5、把data赋给来mes.Data字段
	mes.Data = string(data)

	//6、将mes进行序列化
	data,err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" mes json.Marshal err=", err)
		return
	}

	//7、到这个时候,data就是我们要发送的消息

	//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
	//先获取到data的长度-》转成一个表示长度的byte切片
	// 错误写法: conn.Write(len(data))
	//正确写法:将长度转成一个bytes
	//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte //定义一个数组
	binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32

	//发送长度
	n,err := conn.Write(buf[:4])
	if n!= 4|| err!=nil{
		fmt.Println("conn.Write(bytes)fail,err=",err)
		return
	}

	//fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))

	//发送消息本身
	_,err = conn.Write(data)
	if err!=nil{
		fmt.Println("conn.Write(data)fail,err=",err)
		return
	}

	//休眠20
	time.Sleep(20 * time.Second)
	fmt.Println("休眠了20s.......")


	// 这里还需要处理服务器端返回的消息
	return
}

请添加图片描述

server/main.go 修改

package main

import (
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"go_code/go_code/chapter18/chatroom/common/message"
	"io"
	"net"
)
//在函数头的返回值中声明了 mes 可在函数体中不作声明
func readpkg(conn net.Conn)(mes message.Message,err error){

	buf := make([]byte,8096)
	fmt.Println("开始读取客户端发送的数据......")
	//conn.Read 在conn没有被关闭的情况下,才会阻塞
	//如果客户端关闭了conn,则不会阻塞了
	_,err = conn.Read(buf[:4])
	if err != nil {
		fmt.Println("conn.Read(buf[:pkgLen]) err,err=",err) //return或者干别的事情,如果不设置就会阻塞在这个地方
		return
	}
	fmt.Printf("读到的buf=%v\n",buf[:4])
	//根据读到的buf长度,转成一个 uint32类型
	var pkgLen uint32
	pkgLen = binary.BigEndian.Uint32(buf[:4])

	//根据pkgLen 读取消息内容
	n,err := conn.Read(buf[:pkgLen])
	if n!=int(pkgLen) {
		err = errors.New("read pkg body error")
		//fmt.Println("conn.Read(buf[:pkgLen]) err,err=",err)
		return
	}else if err != nil{
		fmt.Println("conn.Read(buf[:pkgLen]) err ,err=",err)
		return
	}
	//把pkgLen反序列化成->message.Message类型   json.Unmarshal(buf[:pkgLen],mes) mes必须加&。
	//技术就是一层窗户纸,捅出来就不神秘了
	err = json.Unmarshal(buf[:pkgLen],&mes)
	if err != nil {
		fmt.Println("反序列化失败,err=", err)
		return
	}
	return
}

//处理和客户端的通讯
func process(conn net.Conn){
	//这里需要延时关闭conn,否则后边出现非常奇怪的现象,代码多了不好调
	defer conn.Close()

	//循环的读取客户端发送的信息
	for{
		//这里将读取数据包,直接封装成一个函数readpkg  message,Err
		mes,err:=readpkg(conn)
		if err != nil {
			if err ==io.EOF{
				fmt.Println("客户端退出,服务器端也正常退出")
				return
			}else{
				fmt.Println("readpkg err=", err)
			}
		}
		fmt.Println("mes=",mes)
	}
}



func main() {

	//提示信息
	fmt.Println("服务器再8889端口监听......")
	Listen, err := net.Listen("tcp", "127.0.0.1:8889")
	defer Listen.Close()
	if err != nil {
		fmt.Println("net.listen err=", err)
		return
	}
	//一旦监听成功,就等待客户端来链接服务器
	for{
		fmt.Println("等待客户端来链接服务器......")
		conn,err := Listen.Accept()
		if err != nil {
			fmt.Println("conn.Accept err=", err)
		}
		//一旦链接成功,则启动一个协程和客户端保持数据的通讯......
		go process(conn)
	}
}

请添加图片描述
将读取包的任务封装到了一个函数中.readPkg()
请添加图片描述
请添加图片描述

能够完成登录,并提示相应信息

server/main.go 修改
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
client/utils.go 新增
请添加图片描述
请添加图片描述
请添加图片描述
client/login.go 增加代码
请添加图片描述

程序结构的改进, 前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进。
    1. 先改进服务端, 先画出程序的框架图[思路],再写代码.
      请添加图片描述
    1. 步骤
      [1] . 先把分析出来的文件,创建好,然后放到相应的文件夹[包]
      请添加图片描述
      [2] 现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可。
      [3] 先修改了 utils/utils.go
      请添加图片描述
      请添加图片描述
      请添加图片描述
      [4] 修改了 process2/userProcess.go
      请添加图片描述
      请添加图片描述
      请添加图片描述
      [5] 修改了 main/processor.go
      请添加图片描述
      请添加图片描述
      请添加图片描述
      [6] 修改 main/main.go
      请添加图片描述
修改客户端, 先画出程序的框架图[思路],再写代码

[1] 步骤 1-画出示意图
请添加图片描述
[2] 先把各个文件放到对应的文件夹[包]
请添加图片描述


login.go

package main

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"go_code/go_code/chatroom/client/utils"
	"go_code/go_code/chatroom/common/message"
	"net"
)

//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd  string) (err error){

	//下一步就要开始定协议(大事)
	//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)

	//return nil
	//1、链接服务器
	conn,err := net.Dial("tcp","localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()
	//2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType
	//3、创建一个LoginMesTypMes结构体
	var loginMes message.LoginMes
	loginMes.UserId = UserId
	loginMes.UserPwd = UserPwd

	//4、将loginMes序列化
	data,err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("loginMes json.Marshal err=", err)
		return
	}
	//5、把data赋给来mes.Data字段
	mes.Data = string(data)

	//6、将mes进行序列化
	data,err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" mes json.Marshal err=", err)
		return
	}

	//7、到这个时候,data就是我们要发送的消息

	//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
	//先获取到data的长度-》转成一个表示长度的byte切片
	// 错误写法: conn.Write(len(data))
	//正确写法:将长度转成一个bytes
	//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte //定义一个数组
	binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32

	//发送长度
	n,err := conn.Write(buf[:4])
	if n!= 4|| err!=nil{
		fmt.Println("conn.Write(bytes)fail,err=",err)
		return
	}

	//fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))

	//发送消息本身
	_,err = conn.Write(data)
	if err!=nil{
		fmt.Println("conn.Write(data)fail,err=",err)
		return
	}

	//休眠20
	//time.Sleep(20 * time.Second)
	//fmt.Println("休眠了20s.......")

	//处理服务器返回的消息
	mes,err = utils.readpkg(conn) //mes 就是
	if err != nil {
		fmt.Println("如果readpkg(conn) is error err=", err)
		return
	}
	//将mes的Data部分反序列化成loginResMes
	var loginResMes message.LoginResMes
	err =json.Unmarshal([]byte(mes.Data),&loginResMes)
	if err != nil {
		fmt.Println("Unmarshal([]byte(mes.Data),&loginResMes) is error,err=", err)
		return
	}
	if loginResMes.Code == 200{
		fmt.Println("登录成功")
	}else if loginResMes.Code == 500{
		fmt.Println(loginResMes.Error)
	}
 
	return
}

[3] 将 server/utils.go 拷贝到 client/utils/utils.go

[4] 创建了 server/process/userProcess.go
请添加图片描述
说明:该文件就是在原来的 login.go 做了一个改进,即封装到 UserProcess 结构体

[5] 创建了 server/process/server.go
请添加图片描述
请添加图片描述
[6] server/main/main.go 修改
请添加图片描述

在 Redis 手动添加测试用户,并画图+说明注意. (后面通过程序注册用户)

请添加图片描述
请添加图片描述

MVC main.go是现实就代表V processor.go处理器代表C model就代表M,但是在项目复杂的时候还有一个service层,本项目比较简单所用有userDao.go就够了

请添加图片描述

如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的提示信息:
  1. 用户不存在,你也可以重新注册,再登录
  2. 你密码不正确。。
    代码实现:
    [1] 编写 model/user.go
    请添加图片描述
    [2] 编写 model/error.go
    请添加图片描述
    [3] 编写 model/userDao.go
package model

import (
	"encoding/json"
	"fmt"
	"github.com/garyburd/redigo/redis"
)

//我们在服务器启动后,就初始化一个userDao实例,
//把它做成一个全局的变量,在需要和redis操作时就直接使用即可
var (
	MyUserDao *UserDao

)


//定义一个UserDao 结构体体
//完成对User 结构体的各种操作

type UserDao struct{
	pool *redis.Pool
}

//使用工厂模式,创建一个UserDao的实例
func NewUserDao(pool *redis.Pool)(userDao *UserDao){
	userDao = &UserDao{
		pool:pool,
	}
	return
}


//思考一下在UserDao中应该提供哪些方法
//1、根据一个用户ID,返回一个User实例+err
func (dao *UserDao) getUserById(conn redis.Conn,id int)(user *User,err error){
	//通过给定的id去redis查询用户
	res,err := redis.String(conn.Do("HGet","users",id))
	if err != nil {
		if err == redis.ErrNil{ //表示在users 哈希中没有找到对应的ID
			err = ERROR_USER_NOTEXITS
		}
		fmt.Println("Do.HGet is error,err=", err)
		return
	}
	user = &User{}//struct实例是值类型,不需要使用&User{}获取空间
	//这里需要把res 反序列化成一个User实例
	err = json.Unmarshal([]byte(res),user)
	if err != nil {
		fmt.Println("json.Unmarshal([]byte(res),user) is error,err=", err)
		return
	}
	return
}

//完成登录校验
//1、Login 完成对用户的校验
//2、如果用户的id和pwd都正确,则返回一个user实例
//3、如果用户的id或者密码有错误,则返回对应的错误信息
func (this *UserDao) Login(userId int,userPwd string)(user *User,err error){

	//先从UserDao的连接池中取出一根连接
	conn := this.pool.Get()
	defer conn.Close()
	user,err = this.getUserById(conn,userId)
	if err != nil {
		return
	}

	//这时证明这个用户是获取到的,但是密码不一定正确
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}

[4] main/redis.go
请添加图片描述
[5] main/main.go
请添加图片描述
[6] 在 process/userProcess.go 使用到 redis 验证的功能
请添加图片描述

18.5.7 实现功能-完成注册用户
  1. 完成注册功能,将用户信息录入到 Redis 中
  2. 思路分析,并完成代码
  3. 思路分析的示意图

请添加图片描述

实现功能-完成注册用户

[1] common/message/user.go
请添加图片描述
[2] common/message/message.go
请添加图片描述

[3] client/process/userProcess.go

func (this *UserProcess) Register(userId int,
	userPwd string, userName string) (err error) {
//1. 链接到服务器 
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
	fmt.Println("net.Dial err=", err)
	return
} //延时关闭 defer conn.Close()
//2. 准备通过 conn 发送消息给服务 
var mes message.Message 
mes.Type = message.RegisterMesType 
//3. 创建一个 LoginMes 结构体 
var registerMes message.RegisterMes 
registerMes.User.UserId = userId 
registerMes.User.UserPwd = userPwd 
registerMes.User.UserName = userName
//4.将 registerMes 序列化 
data, err := json.Marshal(registerMes) 
if err != nil {
	fmt.Println("json.Marshal err=", err)
	return 
}
// 5. 把 data 赋给 mes.Data 字段
 mes.Data = string(data)
// 6. 将 mes 进行序列化化 d
ata, err = json.Marshal(mes) 
if err != nil {
	fmt.Println("json.Marshal err=", err)
	return 
}
//创建一个 Transfer 实例 
tf := &utils.Transfer{
	Conn : conn, 
}
//发送 data 给服务器端 
err = tf.WritePkg(data) 
if err != nil {
	fmt.Println("注册发送信息错误 err=", err) 
}
mes, err = tf.ReadPkg() // mes 就是 RegisterResMes
if err != nil {
	fmt.Println("readPkg(conn) err=", err)
	return 
}
//将 mes 的 Data 部分反序列化成 RegisterResMes
var registerResMes message.RegisterResMes 
err = json.Unmarshal([]byte(mes.Data), &registerResMes)
 if registerResMes.Code == 200 {
	fmt.Println("注册成功, 你重新登录一把")
	os.Exit(0) 
} else {
	fmt.Println(registerResMes.Error)
	os.Exit(0) 
	} 
	return
}

[4] 在 client/main/main.go 增加了代码
请添加图片描述
[5] 在 server/model/userDao.go 增加方法
请添加图片描述
[6] 在 server/process/userProcess.go 增加了方法,处理注册

package model

import (
	"encoding/json"
	"fmt"
	"github.com/garyburd/redigo/redis"
	"go_code/go_code/chatroom/common/message"
)

//我们在服务器启动后,就初始化一个userDao实例,
//把它做成全局的变量,在需要和redis操作时,就直接使用即可
var (
	MyUserDao *UserDao
)

//定义一个UserDao 结构体体
//完成对User 结构体的各种操作.

type UserDao struct {
	pool  *redis.Pool
}

//使用工厂模式,创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {

	userDao = &UserDao{
		pool: pool,
	}
	return
}

//思考一下在UserDao 应该提供哪些方法给我们
//1. 根据用户id 返回 一个User实例+err
func (this *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {

	//通过给定id 去 redis查询这个用户
	res, err := redis.String(conn.Do("HGet", "users", id))
	if err != nil {
		//错误!
		if err == redis.ErrNil { //表示在 users 哈希中,没有找到对应id
			err = ERROR_USER_NOTEXITS
		}
		return
	}
	user = &User{}
	//这里我们需要把res 反序列化成User实例
	err = json.Unmarshal([]byte(res), user)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err)
		return
	}
	return
}

//完成登录的校验 Login
//1. Login 完成对用户的验证
//2. 如果用户的id和pwd都正确,则返回一个user实例
//3. 如果用户的id或pwd有错误,则返回对应的错误信息
func (this *UserDao) Login(userId int, userPwd string) (user *User, err error) {

	//先从UserDao 的连接池中取出一根连接
	conn := this.pool.Get()
	defer conn.Close()
	user, err = this.getUserById(conn, userId)
	if err != nil {
		return
	}
	//这时证明这个用户是获取到.
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}


func (this *UserDao) Register(user *message.User) (err error) {

	//先从UserDao 的连接池中取出一根连接
	conn := this.pool.Get()
	defer conn.Close()
	_, err = this.getUserById(conn, user.UserId)
	if err == nil {
		err = ERROR_USER_EXISTS
		return
	}
	//这时,说明id在redis还没有,则可以完成注册
	data, err := json.Marshal(user) //序列化
	if err != nil {
		return
	}
	//入库
	_, err = conn.Do("HSet", "users", user.UserId, string(data))
	if err != nil {
		fmt.Println("保存注册用户错误 err=", err)
		return
	}
	return
}

[7] server/main/processor.go 调用了
请添加图片描述

18.5.8 实现功能-完成登录时能返回当前在线用户
用户登录后,可以得到当前在线用户列表思路分析、示意图、代码实现

思路分析:
请添加图片描述
代码实现:
[1] 编写了 server/process/userMgr.go

package process2

import "fmt"

//因为UserMgr 实例在服务器端有且只有一个
//因为在很多的地方,都会使用到,因此,我们
//将其定义为全局变量
var (
	userMgr *UserMgr
)

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

// 完成对userMgr初始化工作
func init() {
	userMgr = &UserMgr{
		onlineUsers: make(map[int]*UserProcess, 1024),
	}
}

//完成对onlineUsers的增 改
func (this *UserMgr) AddOnlineUser(up *UserProcess) {
	this.onlineUsers[up.UserId] = up
}

//完成对onlineUsers的删
func (this *UserMgr) DelOnlineUser(userId int) {
	delete(this.onlineUsers, userId)
}

//完成对onlineUsers的查
//返回当前所有在线的用户
func (this *UserMgr) GetAllOnlineUser() map[int]*UserProcess {
	return this.onlineUsers
}

//针对点对点聊天,根据Id返回对应的UserProcess
func (this *UserMgr) GetOnlineUserById(userId int) (up *UserProcess,err error) {

	//如何从map中取出一个值,带检测的方式
	up,ok := this.onlineUsers[userId]
	if !ok {//说明你要查找的这个用户当前不在线
		err = fmt.Errorf("你要查找的这个用户%d当前不在线",userId)
		return
	}
	return
}

[2] server/process/userProcess.go
请添加图片描述
[3] common/message/message.go
请添加图片描述

[4] client/process/userProcess.go

package process

import (
	"encoding/json"
	"fmt"
	"go_code/go_code/chatroom/client/utils"
	"go_code/go_code/chatroom/common/message"
	"net"
	"os"
)


type UserProcess struct{
	//暂时不需要字段
}

//关联一个用户登录的方法
//写一个函数,完成一个登录校验
func (this *UserProcess) Login(UserId int, UserPwd  string) (err error){

	//下一步就要开始定协议(大事)
	//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)

	//return nil
	//1、链接服务器
	conn,err := net.Dial("tcp","localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()
	//2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType
	//3、创建一个LoginMesTypMes结构体
	var loginMes message.LoginMes
	loginMes.UserId = UserId
	loginMes.UserPwd = UserPwd

	//4、将loginMes序列化
	data,err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("loginMes json.Marshal err=", err)
		return
	}
	//5、把data赋给来mes.Data字段
	mes.Data = string(data)

	//6、将mes进行序列化
	data,err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" mes json.Marshal err=", err)
		return
	}

	/*7、到这个时候,data就是我们要发送的消息

	//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
	//先获取到data的长度-》转成一个表示长度的byte切片
	// 错误写法: conn.Write(len(data))
	//正确写法:将长度转成一个bytes
	//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte //定义一个数组
	binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32

	//发送长度
	n,err := conn.Write(buf[:4])
	if n!= 4|| err!=nil{
		fmt.Println("conn.Write(bytes)fail,err=",err)
		return
	}

	fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))

	//发送消息本身
	_,err = conn.Write(data)
	if err!=nil{
		fmt.Println("conn.Write(data)fail,err=",err)
		return
	}
	 */
	//以上代码封装进了Writerpkg()
	//创建一个Transferi实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	//发送data给服务器
	err = tf.Writepkg(data)
	if err != nil {
		fmt.Println("注册发送信息 is error,err=", err)
	}


	//休眠20
	//time.Sleep(20 * time.Second)
	//fmt.Println("休眠了20s.......")

	//处理服务器返回的消息
	//创建一个Transfer实例
	tf = &utils.Transfer{
		Conn:conn,
	}
	mes,err = tf.Readpkg() //mes 就是LoginResMes
	if err != nil {
		fmt.Println("如果readpkg(conn) is error err=", err)
		return
	}
	//8、将mes的Data部分反序列化成loginResMes
	var loginResMes message.LoginResMes
	err =json.Unmarshal([]byte(mes.Data),&loginResMes)
	if err != nil {
		fmt.Println("Unmarshal([]byte(mes.Data),&loginResMes) is error,err=", err)
		return
	}
	if loginResMes.Code == 200{

		//fmt.Println("登录成功")

		//可以显示当前用户列表
		fmt.Println("当前在线列表如下:")
		for _,v := range loginResMes.UserId{

			//如果不显示自己在线
			if v == UserId{
				continue
			}
			fmt.Printf("用户id%d在线!\t",v)
		}
		fmt.Print("\n\n")
		//在客户端启动一个协程
		//该协程保持和服务器端的一个通讯.如果服务器有数据推送给客户端
		//则接收并显示在客户单的终端
		go serverProcesee(conn)
		//1、循环显示登录成功后的菜单......
		ShowMenu()

	}else {
		fmt.Println(loginResMes.Error)
	}

	return
}

func (this *UserProcess) Register (UserId int,UserPwd ,UserName string) (err error){
	//1、链接服务器
	conn,err := net.Dial("tcp","localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()

	//2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.RegisterMesType
	//3、创建一个RegisterMes结构体
	var registerMes message.RegisterMes
	registerMes.User.UserId = UserId
	registerMes.User.UserPwd = UserPwd
	registerMes.User.UserPwd = UserName
	//4、将RegisterMes序列化
	data,err := json.Marshal(registerMes)
	if err != nil {
		fmt.Println("loginMes json.Marshal err=", err)
		return
	}
	//5、把data赋给来mes.Data字段
	mes.Data = string(data)

	//6、将mes进行序列化
	data,err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" mes json.Marshal err=", err)
		return
	}
	//创建一个Transferi实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	//7、发送data给服务器
	err = tf.Writepkg(data)
	if err != nil {
		fmt.Println("注册发送信息 is error,err=", err)
	}
	//创建一个Transferi实例
	tf = &utils.Transfer{
		Conn:conn,
	}
	//8、读取服务器发送的信息
	mes,err = tf.Readpkg() //mes 就是RegisterResMes
	if err != nil {
		fmt.Println("如果Writepkg(conn) is error err=", err)
		return
	}
	//8、将mes的Data部分反序列化成RegisterResMes
	var registerResMes message.RegisterResMes
	err =json.Unmarshal([]byte(mes.Data),&registerResMes)
	if err != nil {
		fmt.Println("Unmarshal([]byte(mes.Data),&registerResMes) is error,err=", err)
		return
	}
	if registerResMes.Code == 200{
		fmt.Println("注册成功,你重新登录一下")
		os.Exit(0)
	}else {
		fmt.Println(registerResMes.Error)
		os.Exit(0)
	}
	return
}
当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表,思路分析、示意图、代码实现

请添加图片描述

[1] server/process/userProcess.go

package process2

import (
	"encoding/json"
	"fmt"
	"go_code/go_code/chatroom/common/message"
	"go_code/go_code/chatroom/server/model"
	"go_code/go_code/chatroom/server/utils"
	"net"
)

type UserProcess struct {
	//字段
	Conn net.Conn
	//增加一个字段,表示该Conn是哪个用户
	UserId int
}

//这里我们编写通知所有在线的用户的方法
//userId 要通知其它的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {

	//遍历 onlineUsers, 然后一个一个的发送 NotifyUserStatusMes
	for id, up := range userMgr.onlineUsers {
		//过滤到自己
		if id == userId {
			continue
		}
		//开始通知【单独的写一个方法】
		up.NotifyMeOnline(userId)
	}
}

func (this *UserProcess) NotifyMeOnline(userId int) {

	//组装我们的NotifyUserStatusMes
	var mes message.Message
	mes.Type = message.NotifyUserStatusMesType

	var notifyUserStatusMes message.NotifyUserStatusMes
	notifyUserStatusMes.UserId = userId
	notifyUserStatusMes.UserStatus = message.UserOnline

	//将notifyUserStatusMes序列化
	data, err := json.Marshal(notifyUserStatusMes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}
	//将序列化后的notifyUserStatusMes赋值给 mes.Data
	mes.Data = string(data)

	//对mes再次序列化,准备发送.
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//发送,创建我们Transfer实例,发送
	tf := &utils.Transfer{
		Conn : this.Conn,
	}

	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("NotifyMeOnline err=", err)
		return
	}
}



//编写一个函数serverProcessLogin函数, 专门处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
	//核心代码...
	//1. 先从mes 中取出 mes.Data ,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data), &loginMes)
	if err != nil {
		fmt.Println("json.Unmarshal fail err=", err)
		return
	}
	//1先声明一个 resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType
	//2在声明一个 LoginResMes,并完成赋值
	var loginResMes message.LoginResMes

	//我们需要到redis数据库去完成验证.
	//1.使用model.MyUserDao 到redis去验证
	user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)

	if err != nil {

		if err == model.ERROR_USER_NOTEXITS{
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		}else if err == model.ERROR_USER_PWD{
			loginResMes.Code = 300
			loginResMes.Error = err.Error()
		}else {
			loginResMes.Code = 505
			loginResMes.Error = "服务器内部错误"
		}

	} else {
		loginResMes.Code = 200
		//这里,因为用户登录成功,我们就把该登录成功的用放入到userMgr中
		//将登录成功的用户的userId 赋给 this
		this.UserId = loginMes.UserId
		userMgr.AddOnlineUser(this)
		通知其它的在线用户, 我上线了
		this.NotifyOthersOnlineUser(loginMes.UserId)
		//将当前在线用户的id 放入到loginResMes.UsersId
		//遍历 userMgr.onlineUsers
		for id, _ := range userMgr.onlineUsers {
			loginResMes.UserId = append(loginResMes.UserId, id)
		}
		fmt.Println(user, "登录成功")
	}
	// //如果用户id= 100, 密码=123456, 认为合法,否则不合法

	// if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
	// 	//合法
	// 	loginResMes.Code = 200

	// } else {
	// 	//不合法
	// 	loginResMes.Code = 500 // 500 状态码,表示该用户不存在
	// 	loginResMes.Error = "该用户不存在, 请注册再使用..."
	// }

	//3将 loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail", err)
		return
	}

	//4. 将data 赋值给 resMes
	resMes.Data = string(data)

	//5. 对resMes 进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail", err)
		return
	}
	//6. 发送data, 我们将其封装到writePkg函数
	//因为使用分层模式(mvc), 我们先创建一个Transfer 实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}
	err = tf.WritePkg(data)
	return
}

func (this *UserProcess) ServerProcessRegister(mes *message.Message)(err error){
	//核心代码...
	//1. 先从mes 中取出 mes.Data ,并直接反序列化成RegisterMes
	var registerMes message.RegisterMes
	err = json.Unmarshal([]byte(mes.Data), &registerMes)
	if err != nil {
		fmt.Println("ServerProcessRegister() json.Unmarshal fail err=", err)
		return
	}
	//1先声明一个 resMes
	var resMes message.Message
	resMes.Type = message.RegisterResMesType
	//2在声明一个 RegisterResMes,并完成赋值
	var registerResMes message.RegisterResMes

	//我们需要到redis数据库去完成注册.
	//1.使用model.MyUserDao 到redis去验证
	err = model.MyUserDao.Register(&registerMes.User)

	if err != nil {

		if err == model.ERROR_USER_EXISTS{
			registerResMes.Code = 505
			registerResMes.Error = err.Error()
		} else {
			registerResMes.Code = 506
			registerResMes.Error = "注册发送未知错误!"
		}

	} else {
		registerResMes.Code = 200
		fmt.Println( registerMes.User.UserId,"注册登录成功")
	}

	//3将 regesterResMes 序列化
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("registerResMes json.Marshal fail", err)
		return
	}

	//4. 将data 赋值给 resMes
	resMes.Data = string(data)

	//5. 对resMes 进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail", err)
		return
	}
	//6. 发送data, 我们将其封装到writePkg函数
	//因为使用分层模式(mvc), 我们先创建一个Transfer 实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}
	err = tf.WritePkg(data)
	return
}

[2] sever/proces/userProcess.go [的 Login]
请添加图片描述
[3] common/mesage/message.go
请添加图片描述
[4] client/process/userMgr.go

package process

import (
	"fmt"
	"go_code/go_code/chatroom/common/message"
)

//客户端维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User,10)

//在客户端显示当前在线的用户
func outputOnlineUser(){
	//遍历一把
	fmt.Println("当前在线用户列表:")
	for id,_ := range onlineUsers{
		fmt.Println("用户Id:\t",id)
	}
}



//编写一个方法,处理返回的NotifyStatusMes
func UpdateUserStatus (notifyUserStatusMes *message.NotifyUserStatusMes){

	//效率比较低,原先可能存在这个user
	//user := &message.User{
	//	UserId : notifyUserStatusMes.UserId,
	//	UserStatus: notifyUserStatusMes.UserStatus,
	//}
	//onlineUsers[notifyUserStatusMes.UserId] = user

	//适当优化
	user , ok := onlineUsers[notifyUserStatusMes.UserId]
	if !ok {//原来没有
		user = &message.User{
			UserId : notifyUserStatusMes.UserId,
			//UserStatus: notifyUserStatusMes.UserStatus,
		}
	}
	user.UserStatus = notifyUserStatusMes.UserStatus
	onlineUsers[notifyUserStatusMes.UserId] = user
	//在客户端显示当前在线的用户
	outputOnlineUser()
}

[5] client/process/server.go

package process

import (
	"encoding/json"
	"fmt"
	"go_code/go_code/chatroom/common/message"
	"go_code/go_code/chatroom/server/utils"
	"net"
	"os"
)

//1、显示登录成功后的界面...
func ShowMenu(){

	for{

		fmt.Println("------恭喜xxx登录成功------")
		fmt.Println("------1、显示用户在线列表------")
		fmt.Println("------2、发送消息------")
		fmt.Println("------3、信息列表------")
		fmt.Println("------4、退出系统------")
		fmt.Println("请选择(1-4):")
		var choose int
		fmt.Scanf("%d\n",&choose)
		switch choose{
			case 1 :
				fmt.Println("显示在线用户列表:")
				outputOnlineUser()
			case 2 :
				fmt.Println("发送消息-")
			case 3 :
				fmt.Println("查看聊天历史记录(存在文件中)-")
			case 4 :
				fmt.Println("你选择退出了系统-")
				os.Exit(0)
			default:
				fmt.Println("输入错误,请重新输入!")
		}

	}

}

//2、保持和服务器的通讯
func serverProcesee(conn net.Conn){
	//创建一个Transfer实例,不停的读取服务器消息
	tf := &utils.Transfer{
		Conn: conn,
	}
	for{
		//客户端不停的在读取
		fmt.Printf("客户端xxx正在等待读取服务器发送的消息")
		mes,err := tf.ReadPkg()
		if err != nil {
			fmt.Println("tf.Readpkg(),err=", err)
			return
		}
		//如果读取到消息,就是下一步处理逻辑
		//fmt.Println("mes=",mes)
		switch mes.Type{
			case message.NotifyUserStatusMesType:
				//通知有人上线了
				//1、取出这个NotifyUserStatus
				var notifyUserStatusMes message.NotifyUserStatusMes
				json.Unmarshal([]byte(mes.Data),&notifyUserStatusMes)
				//2、把这个用户的状态保存到客户端维护的map[int]User中去
				UpdateUserStatus(&notifyUserStatusMes)
			default:
				fmt.Println("服务器端返回了一个未知的消息类型")
		}
	}
}

请添加图片描述

[6] client/process/server.go
请添加图片描述
请添加图片描述

18.5.9 实现功能-完成登录用可以群聊
步骤 1:步骤 1:当一个用户上线后,可以将群聊消息发给服务器,服务器可以接收到

思路分析:
请添加图片描述
代码实现:
[1] common/message/messag.go

请添加图片描述

[2] client/model/curUser.go
请添加图片描述
[3] client/process/smsProcess.go 增加了发送群聊消息
请添加图片描述
请添加图片描述
请添加图片描述
[4] 测试
请添加图片描述

步骤 2:服务器可以将接收到的消息,群发给所有在线用户(发送者除外)

思路分析:
请添加图片描述
代码实现:
[1] server/process/smsProcess.go
请添加图片描述
请添加图片描述
[2] server/main/processor.go
请添加图片描述
[3] client/process/smsMgr.go
请添加图片描述
[4] client/process/server.go
请添加图片描述

18.5.10 聊天的项目的扩展功能要求
  1. 实现私聊.[点对点聊天]
  2. 如果一个登录用户离线,就把这个人从在线列表去掉【】
  3. 实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受离线的消息
  4. 发送一个文件.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值