• [go学习笔记.第十六章.TCP编程] 2.项目-海量用户即时通讯系统


    一.项目介绍

    1.项目开发流程

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

    2.需求分析 

    (1).用户注册

    (2).用户登录

    (3).显示在线用户列表

    (4).群聊(广播)

    (5).点对点聊天

    (6).离线留言

    3.示意图

     4.项目开发前技术准备 

    项目要保存用户信息和消息数据,因此需要数据库(mysql或者redis),这里使用redis进行数据的保存

    二.项目开发 

    1.实现功能:显示客户端登录菜单

    功能:

            能够正确地显示客户端的菜单

    界面如下:

     代码如下:

      client/login.go

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. //写一个函数, 完成登录
    6. func login(userId int, userPwd string) (err error) {
    7. //下一步 开始定协议
    8. fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    9. return
    10. }

    client/main.go

    1. package main
    2. import (
    3. "fmt"
    4. "os"
    5. )
    6. //定义两个全局变量,一个用户id,一个用户密码
    7. var userId int
    8. var userPwd string
    9. func main() {
    10. //接收用户的选择
    11. var key int
    12. //判断是否还继续显示菜单
    13. var loop = true
    14. for loop {
    15. fmt.Println("-------------------欢迎登录多人聊天系统-------------------")
    16. fmt.Println("\t\t\t\t 1 登录聊天室")
    17. fmt.Println("\t\t\t\t 2 注册用户")
    18. fmt.Println("\t\t\t\t 3 退出系统")
    19. fmt.Println("\t\t\t\t 请选择(1~3):")
    20. fmt.Scanf("%d\n", &key)
    21. switch key {
    22. case 1 :
    23. fmt.Println("登录聊天室")
    24. loop = false
    25. case 2 :
    26. fmt.Println("注册用户")
    27. loop = false
    28. case 3 :
    29. fmt.Println("退出系统")
    30. os.Exit(0)
    31. default:
    32. fmt.Println("输入有误,请重新输入")
    33. }
    34. }
    35. //根据用户的输入,显示新的提示信息
    36. if key == 1 {
    37. //说明用户要登录
    38. fmt.Println("请输入用户的id:")
    39. fmt.Scanf("%d\n", &userId)
    40. fmt.Println("请输入用户的密码:")
    41. fmt.Scanf("%s\n", &userPwd)
    42. //先把登录的函数写到另一个文件,比如:login.go
    43. err := login(userId, userPwd)
    44. if err != nil {
    45. fmt.Println("登录失败")
    46. } else {
    47. fmt.Println("登录成功")
    48. }
    49. } else if key == 2{
    50. fmt.Println("注册")
    51. }
    52. }

    2.实现功能:完成用户登录

    要求: 

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

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

    分析思路

    (1).先确定消息 Message 的格式和结构

    (2).根据上图的分析完成代码

    (3).示意图如下

     server/main.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. )
    6. //处理和客户端通讯
    7. func process(conn net.Conn) {
    8. //这里需要延时关闭
    9. defer conn.Close()
    10. //读取客户端发送的消息
    11. for {
    12. buf := make([]byte, 8096)
    13. fmt.Println("读取客户端发送的数据...")
    14. n, err := conn.Read(buf[:4])
    15. if err != nil || n != 4 {
    16. fmt.Printf(" conn read err = %v\n", err)
    17. return
    18. }
    19. fmt.Printf("读到的buf= %v\n", buf[:4])
    20. }
    21. }
    22. func main() {
    23. //提示信息
    24. fmt.Println("服务器正在监听8889端口...")
    25. listen, err := net.Listen("tcp", "127.0.0.1:8889")
    26. //这里需要延时关闭
    27. defer listen.Close()
    28. if err != nil {
    29. fmt.Printf("net listen err = %v\n", err)
    30. return
    31. }
    32. //一旦监听成功,就等待客户端来连接服务器
    33. for {
    34. fmt.Println("等待客户端来连接服务器...")
    35. conn, err := listen.Accept()
    36. if err != nil {
    37. fmt.Printf("listen accept err = %v\n", err)
    38. return
    39. }
    40. //一旦连接成功,则启动一个协程,保持和客户端通讯
    41. go process(conn)
    42. }
    43. }

     common/message/message.go

    1. package message
    2. //定义消息类型
    3. const (
    4. LoginMesType = "LoginMes"
    5. LoginResMesType = "LoginResMes"
    6. )
    7. type Message struct {
    8. Type string `json:"type"` // 消息类型
    9. Data string `json:"data"` //消息内容
    10. }
    11. //定义需要的消息
    12. type LoginMes struct {
    13. UserId int `json:"userId"`//用户id
    14. UserPwd string `json:"userPwd"` //用户密码
    15. UserName string `json:"userName"` //用户名
    16. }
    17. type LoginResMes struct {
    18. Code int `json:"code"` //返回状态码: 200 登录成功, 500 用户未注册
    19. Error string `json:"error"` //返回错误信息
    20. }

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

    clent/login.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. "encoding/binary"
    6. "encoding/json"
    7. "go_code/chatroom/common/message"
    8. )
    9. //写一个函数, 完成登录
    10. func login(userId int, userPwd string) (err error) {
    11. // //下一步 开始定协议
    12. // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    13. // return
    14. //1.连接到服务器端
    15. conn, err := net.Dial("tcp", "127.0.0.1:8889")
    16. if err != nil {
    17. fmt.Printf("net dial err = %v\n", err)
    18. return
    19. }
    20. //延时关闭
    21. defer conn.Close()
    22. //2.准备通过conn发送消息给服务端
    23. var mes message.Message
    24. mes.Type = message.LoginMesType
    25. //3.创建一个LoginMes结构体
    26. var loginMes message.LoginMes
    27. loginMes.UserId = userId
    28. loginMes.UserPwd = userPwd
    29. //4.将loginMes序列化
    30. data, err := json.Marshal(loginMes)
    31. if err != nil {
    32. fmt.Printf("json marshal err = %v\n", err)
    33. return
    34. }
    35. //5.将序列化后的loginMes byte切片赋给mes.Data
    36. mes.Data = string(data)
    37. //6.将mes序列化
    38. data, err = json.Marshal(mes)
    39. if err != nil {
    40. fmt.Printf("json marshal err = %v\n", err)
    41. return
    42. }
    43. //7.这个时候data就是要发送的消息
    44. //7.1先把data的长度发送给服务器
    45. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    46. var pkgLen uint32
    47. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    48. var buf [4]byte
    49. binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    50. //发送长度
    51. n, err := conn.Write(buf[:4])
    52. if err != nil || n != 4 {
    53. fmt.Printf(" conn write err = %v\n", err)
    54. return
    55. }
    56. fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data))
    57. return
    58. }

    2).完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes),判断用户的合法性,并返回相应的LoginResMes

    思路分析:

    (1).让客户端发送消息本身

    (2).服务器端接受到消息,然后反序列话化成对应的消息结构体

    (3).服务器端根据反序列化成对应的消意,判断登录用户是合法,返回LoginResMes

    (4).客户端解析返回的LoginResMes ,显示对应界面

    (5).这里需要做函数的封装

    client/login.go做了点修改 

    1. // fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data))
    2. //发送消息本身
    3. _, err = conn.Write(data)
    4. if err != nil {
    5. fmt.Printf(" conn write err = %v\n", err)
    6. return
    7. }
    8. //休眠20s
    9. time.Sleep(20 * time.Second)
    10. fmt.Println("休眠20s...")
    11. //处理服务端返回的消息
    12. return
    13. }

    server/main.go做了改动

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. "go_code/chatroom/common/message"
    6. "encoding/binary"
    7. "encoding/json"
    8. _"errors"
    9. "io"
    10. )
    11. //处理和客户端通讯
    12. func process(conn net.Conn) {
    13. //这里需要延时关闭
    14. defer conn.Close()
    15. //读取客户端发送的消息
    16. for {
    17. //将读取数据包直接封装成一个函数readPkg(),返回messag,error
    18. mes, err := readPkg(conn)
    19. if err != nil {
    20. if err == io.EOF {
    21. fmt.Println("客户端退出了,服务器端也跟着退出")
    22. } else {
    23. fmt.Printf(" readPkg err = %v\n", err)
    24. }
    25. return
    26. }
    27. fmt.Printf("mes= %v\n", mes)
    28. }
    29. }
    30. //读取数据包直接封装成一个函数readPkg()
    31. func readPkg(conn net.Conn) (mes message.Message, err error) {
    32. //创建一个切片,用于后续conn.Read()
    33. buf := make([]byte, 8096)
    34. fmt.Println("读取客户端发送的数据...")
    35. //conn.Read在conn没有被关闭的情况下,才会阻塞
    36. //如果客户端关闭了conn,则不会阻塞
    37. _, err = conn.Read(buf[:4])
    38. if err != nil {
    39. // err = errors.New("read pkg header error")
    40. return
    41. }
    42. //根据buf[:4]转成一个uint32类型
    43. var pkgLen uint32
    44. pkgLen = binary.BigEndian.Uint32(buf[0:4])
    45. //根据pkgLen读取消息内容
    46. n, err := conn.Read(buf[:pkgLen])
    47. if n != int(pkgLen) || err != nil {
    48. // err = errors.New("read pkg body error")
    49. return
    50. }
    51. //把pkgLen反序列化成message.Message
    52. err = json.Unmarshal(buf[:pkgLen], &mes)
    53. if err != nil {
    54. // err = errors.New("read pkg json.Unmarshal error")
    55. return
    56. }
    57. return
    58. }
    59. func main() {
    60. //提示信息
    61. fmt.Println("服务器正在监听8889端口...")
    62. listen, err := net.Listen("tcp", "127.0.0.1:8889")
    63. //这里需要延时关闭
    64. defer listen.Close()
    65. if err != nil {
    66. fmt.Printf("net listen err = %v\n", err)
    67. return
    68. }
    69. //一旦监听成功,就等待客户端来连接服务器
    70. for {
    71. fmt.Println("等待客户端来连接服务器...")
    72. conn, err := listen.Accept()
    73. if err != nil {
    74. fmt.Printf("listen accept err = %v\n", err)
    75. return
    76. }
    77. //一旦连接成功,则启动一个协程,保持和客户端通讯
    78. go process(conn)
    79. }
    80. }

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

    server/server.go修改

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. "go_code/chatroom/common/message"
    6. "encoding/binary"
    7. "encoding/json"
    8. _"errors"
    9. "io"
    10. )
    11. //处理和客户端通讯
    12. func process(conn net.Conn) {
    13. //这里需要延时关闭
    14. defer conn.Close()
    15. //读取客户端发送的消息
    16. for {
    17. //将读取数据包直接封装成一个函数readPkg(),返回messag,error
    18. mes, err := readPkg(conn)
    19. if err != nil {
    20. if err == io.EOF {
    21. fmt.Println("客户端退出了,服务器端也跟着退出")
    22. } else {
    23. fmt.Printf(" readPkg err = %v\n", err)
    24. }
    25. return
    26. }
    27. err = serverProcessMes(conn, &mes)
    28. if err != nil {
    29. return
    30. }
    31. }
    32. }
    33. //编写一个函数serverProcessLogin函数,专门处理登录请求
    34. func serverProcessLogin(conn net.Conn, mes *message.Message) (err error) {
    35. //先从mes中取出mes.Data,并直接反序列化成LoginMes
    36. var loginMes message.LoginMes
    37. err = json.Unmarshal([]byte(mes.Data), &loginMes)
    38. if err != nil {
    39. fmt.Printf("json.Unmarshal fail, err = %v\n", err)
    40. return
    41. }
    42. //1.声明一个resMes
    43. var resMes message.Message
    44. resMes.Type = message.LoginResMesType
    45. //2.再声明一个loginResMes,并完成赋值
    46. var loginResMes message.LoginResMes
    47. //如果用户id=100,密码=123456,则合法,不然,则不合法
    48. if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
    49. //合法
    50. loginResMes.Code = 200
    51. } else {
    52. //不合法
    53. loginResMes.Code = 500 //500 表示不存在
    54. loginResMes.Error = "该用户不存在,请注册后使用"
    55. }
    56. //3.将loginResMes序列化
    57. data, err := json.Marshal(loginResMes)
    58. if err != nil {
    59. fmt.Printf("json.Marshal fail, err = %v\n", err)
    60. return
    61. }
    62. //4.将data赋值给resMes
    63. resMes.Data = string(data)
    64. //5.对resMes序列化,准备发送
    65. data, err = json.Marshal(resMes)
    66. if err != nil {
    67. fmt.Printf("json.Marshal fail, err = %v\n", err)
    68. return
    69. }
    70. //6.发送data,将其封装到writePkg函数中
    71. err = writePkg(conn, data)
    72. return
    73. }
    74. func writePkg(conn net.Conn, data []byte) (err error) {
    75. //先发送一个长度给对方
    76. //先把data的长度发送给对方
    77. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    78. var pkgLen uint32
    79. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    80. var buf [4]byte
    81. binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    82. //发送长度
    83. n, err := conn.Write(buf[:4])
    84. if err != nil || n != 4 {
    85. fmt.Printf(" conn write err = %v\n", err)
    86. return
    87. }
    88. //发送data本身
    89. n, err = conn.Write(data)
    90. if err != nil || n != int(pkgLen) {
    91. fmt.Printf(" conn write err = %v\n", err)
    92. }
    93. return
    94. }
    95. //编写一个ServerProcessMes函数
    96. //功能:根据客户端发送消息类型不同,决定调用哪个函数来处理
    97. func serverProcessMes(conn net.Conn, mes *message.Message) (err error) {
    98. switch mes.Type {
    99. case message.LoginMesType :
    100. //处理登录消息
    101. err = serverProcessLogin(conn, mes)
    102. case message.RegisterMesType :
    103. //处理注册
    104. default :
    105. fmt.Println("消息类型不存在, 无法处理...")
    106. }
    107. return
    108. }
    109. //读取数据包直接封装成一个函数readPkg()
    110. func readPkg(conn net.Conn) (mes message.Message, err error) {
    111. //创建一个切片,用于后续conn.Read()
    112. buf := make([]byte, 8096)
    113. fmt.Println("读取客户端发送的数据...")
    114. //conn.Read在conn没有被关闭的情况下,才会阻塞
    115. //如果客户端关闭了conn,则不会阻塞
    116. _, err = conn.Read(buf[:4])
    117. if err != nil {
    118. return
    119. }
    120. //根据buf[:4]转成一个uint32类型
    121. var pkgLen uint32
    122. pkgLen = binary.BigEndian.Uint32(buf[0:4])
    123. //根据pkgLen读取消息内容
    124. n, err := conn.Read(buf[:pkgLen])
    125. if n != int(pkgLen) || err != nil {
    126. // err = errors.New("read pkg body error")
    127. return
    128. }
    129. //把pkgLen反序列化成message.Message
    130. err = json.Unmarshal(buf[:pkgLen], &mes)
    131. if err != nil {
    132. return
    133. }
    134. return
    135. }
    136. func main() {
    137. //提示信息
    138. fmt.Println("服务器正在监听8889端口...")
    139. listen, err := net.Listen("tcp", "127.0.0.1:8889")
    140. //这里需要延时关闭
    141. defer listen.Close()
    142. if err != nil {
    143. fmt.Printf("net listen err = %v\n", err)
    144. return
    145. }
    146. //一旦监听成功,就等待客户端来连接服务器
    147. for {
    148. fmt.Println("等待客户端来连接服务器...")
    149. conn, err := listen.Accept()
    150. if err != nil {
    151. fmt.Printf("listen accept err = %v\n", err)
    152. return
    153. }
    154. //一旦连接成功,则启动一个协程,保持和客户端通讯
    155. go process(conn)
    156. }
    157. }

    client/utils/utils.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. "encoding/binary"
    6. "encoding/json"
    7. "go_code/chatroom/common/message"
    8. )
    9. //读取数据包直接封装成一个函数readPkg()
    10. func readPkg(conn net.Conn) (mes message.Message, err error) {
    11. //创建一个切片,用于后续conn.Read()
    12. buf := make([]byte, 8096)
    13. fmt.Println("读取客户端发送的数据...")
    14. //conn.Read在conn没有被关闭的情况下,才会阻塞
    15. //如果客户端关闭了conn,则不会阻塞
    16. _, err = conn.Read(buf[:4])
    17. if err != nil {
    18. // err = errors.New("read pkg header error")
    19. return
    20. }
    21. //根据buf[:4]转成一个uint32类型
    22. var pkgLen uint32
    23. pkgLen = binary.BigEndian.Uint32(buf[0:4])
    24. //根据pkgLen读取消息内容
    25. n, err := conn.Read(buf[:pkgLen])
    26. if n != int(pkgLen) || err != nil {
    27. // err = errors.New("read pkg body error")
    28. return
    29. }
    30. //把pkgLen反序列化成message.Message
    31. err = json.Unmarshal(buf[:pkgLen], &mes)
    32. if err != nil {
    33. // err = errors.New("read pkg json.Unmarshal error")
    34. return
    35. }
    36. return
    37. }
    38. func writePkg(conn net.Conn, data []byte) (err error) {
    39. //先发送一个长度给对方
    40. //先把data的长度发送给对方
    41. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    42. var pkgLen uint32
    43. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    44. var buf [4]byte
    45. binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    46. //发送长度
    47. n, err := conn.Write(buf[:4])
    48. if err != nil || n != 4 {
    49. fmt.Printf(" conn write err = %v\n", err)
    50. return
    51. }
    52. //发送data本身
    53. n, err = conn.Write(data)
    54. if err != nil || n != int(pkgLen) {
    55. fmt.Printf(" conn write err = %v\n", err)
    56. }
    57. return
    58. }

    client/client.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. _"time"
    6. "encoding/binary"
    7. "encoding/json"
    8. "go_code/chatroom/common/message"
    9. )
    10. //写一个函数, 完成登录
    11. func login(userId int, userPwd string) (err error) {
    12. // //下一步 开始定协议
    13. // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    14. // return
    15. //1.连接到服务器端
    16. conn, err := net.Dial("tcp", "127.0.0.1:8889")
    17. if err != nil {
    18. fmt.Printf("net dial err = %v\n", err)
    19. return
    20. }
    21. //延时关闭
    22. defer conn.Close()
    23. //2.准备通过conn发送消息给服务端
    24. var mes message.Message
    25. mes.Type = message.LoginMesType
    26. //3.创建一个LoginMes结构体
    27. var loginMes message.LoginMes
    28. loginMes.UserId = userId
    29. loginMes.UserPwd = userPwd
    30. //4.将loginMes序列化
    31. data, err := json.Marshal(loginMes)
    32. if err != nil {
    33. fmt.Printf("json marshal err = %v\n", err)
    34. return
    35. }
    36. //5.将序列化后的loginMes byte切片赋给mes.Data
    37. mes.Data = string(data)
    38. //6.将mes序列化
    39. data, err = json.Marshal(mes)
    40. if err != nil {
    41. fmt.Printf("json marshal err = %v\n", err)
    42. return
    43. }
    44. //7.这个时候data就是要发送的消息
    45. //7.1先把data的长度发送给服务器
    46. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    47. var pkgLen uint32
    48. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    49. var buf [4]byte
    50. binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    51. //发送长度
    52. n, err := conn.Write(buf[:4])
    53. if err != nil || n != 4 {
    54. fmt.Printf(" conn write err = %v\n", err)
    55. return
    56. }
    57. // fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data))
    58. //发送消息本身
    59. _, err = conn.Write(data)
    60. if err != nil {
    61. fmt.Printf(" conn write err = %v\n", err)
    62. return
    63. }
    64. //休眠20s
    65. // time.Sleep(20 * time.Second)
    66. // fmt.Println("休眠20s...")
    67. //处理服务端返回的消息
    68. mes, err = readPkg(conn)
    69. if err != nil {
    70. fmt.Println("readPkg(conn) err = ", err)
    71. return
    72. }
    73. //将mes中的Data反序列化成LoginResMes
    74. var loginResMes message.LoginResMes
    75. err = json.Unmarshal([]byte(mes.Data), &loginResMes)
    76. if err != nil {
    77. fmt.Println("json.Unmarshal err = ", err)
    78. return
    79. }
    80. if loginResMes.Code == 200 {
    81. fmt.Println("登录成功")
    82. } else if loginResMes.Code == 500 {
    83. fmt.Println(loginResMes.Error)
    84. }
    85. return
    86. }

    程序结构的改进,前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进

    1).先改进服务端.先画出程序的框架图,再写代码

    2).步骤

    (1).先把分析出来的文件,创建好,然后把对应的代码放入到相应的文件夹(包) 

    (2).根据各个文件,完成的任务不同,将main.go的代码剥离到对应的文件中即可

    server/main/main.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. )
    6. //处理和客户端通讯
    7. func process(conn net.Conn) {
    8. //这里需要延时关闭
    9. defer conn.Close()
    10. //这里调用总控,创建一个总控实例
    11. processor := &Processor{
    12. Conn: conn,
    13. }
    14. err := processor.ProcessMain()
    15. if err != nil {
    16. fmt.Printf("客户端和服务器端的协程出问题了,err= %v\n", err)
    17. return
    18. }
    19. }
    20. func main() {
    21. //提示信息
    22. fmt.Println("服务器[新结构]正在监听8889端口...")
    23. listen, err := net.Listen("tcp", "127.0.0.1:8889")
    24. //这里需要延时关闭
    25. defer listen.Close()
    26. if err != nil {
    27. fmt.Printf("net listen err = %v\n", err)
    28. return
    29. }
    30. //一旦监听成功,就等待客户端来连接服务器
    31. for {
    32. fmt.Println("等待客户端来连接服务器...")
    33. conn, err := listen.Accept()
    34. if err != nil {
    35. fmt.Printf("listen accept err = %v\n", err)
    36. return
    37. }
    38. //一旦连接成功,则启动一个协程,保持和客户端通讯
    39. go process(conn)
    40. }
    41. }

     server/main/processor.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. "io"
    6. "go_code/chatroom/common/message"
    7. "go_code/chatroom/server/utils"
    8. "go_code/chatroom/server/processBlock"
    9. )
    10. //创建一个Processor的结构体
    11. type Processor struct {
    12. Conn net.Conn
    13. }
    14. //编写一个ServerProcessMes函数
    15. //功能:根据客户端发送消息类型不同,决定调用哪个函数来处理
    16. func (this *Processor) serverProcessMes(mes *message.Message) (err error) {
    17. switch mes.Type {
    18. case message.LoginMesType :
    19. //处理登录消息
    20. //创建一个UserProcess
    21. up := &processBlock.UserProcess{
    22. Conn: this.Conn,
    23. }
    24. err = up.ServerProcessLogin(mes)
    25. case message.RegisterMesType :
    26. //处理注册
    27. default :
    28. fmt.Println("消息类型不存在, 无法处理...")
    29. }
    30. return
    31. }
    32. func (this *Processor) ProcessMain() (err error) {
    33. //读取客户端发送的消息
    34. for {
    35. //将读取数据包直接封装成一个函数readPkg(),返回messag,error
    36. //创建一个Transfer,完成读包的任务
    37. tf := &utils.Transfer{
    38. Conn: this.Conn,
    39. }
    40. mes, err := tf.ReadPkg()
    41. if err != nil {
    42. if err == io.EOF {
    43. fmt.Println("客户端退出了,服务器端也跟着退出")
    44. } else {
    45. fmt.Printf(" readPkg err = %v\n", err)
    46. }
    47. return err
    48. }
    49. err = this.serverProcessMes(&mes)
    50. if err != nil {
    51. return err
    52. }
    53. }
    54. }

     server/utils/utils.go

    1. package utils
    2. import (
    3. "fmt"
    4. "net"
    5. "go_code/chatroom/common/message"
    6. "encoding/binary"
    7. "encoding/json"
    8. )
    9. //将方法关联到结构体中
    10. type Transfer struct {
    11. Conn net.Conn
    12. Buf [8096]byte //传输时,使用的缓冲
    13. }
    14. func (this *Transfer) WritePkg(data []byte) (err error) {
    15. //先发送一个长度给对方
    16. //先把data的长度发送给对方
    17. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    18. var pkgLen uint32
    19. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    20. binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    21. //发送长度
    22. n, err := this.Conn.Write(this.Buf[:4])
    23. if err != nil || n != 4 {
    24. fmt.Printf(" conn write err = %v\n", err)
    25. return
    26. }
    27. //发送data本身
    28. n, err = this.Conn.Write(data)
    29. if err != nil || n != int(pkgLen) {
    30. fmt.Printf(" conn write err = %v\n", err)
    31. }
    32. return
    33. }
    34. //读取数据包直接封装成一个函数readPkg()
    35. func (this *Transfer) ReadPkg() (mes message.Message, err error) {
    36. fmt.Println("读取客户端发送的数据...")
    37. //conn.Read在conn没有被关闭的情况下,才会阻塞
    38. //如果客户端关闭了conn,则不会阻塞
    39. _, err = this.Conn.Read(this.Buf[:4])
    40. if err != nil {
    41. return
    42. }
    43. //根据buf[:4]转成一个uint32类型
    44. var pkgLen uint32
    45. pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
    46. //根据pkgLen读取消息内容
    47. n, err := this.Conn.Read(this.Buf[:pkgLen])
    48. if n != int(pkgLen) || err != nil {
    49. // err = errors.New("read pkg body error")
    50. return
    51. }
    52. //把pkgLen反序列化成message.Message
    53. err = json.Unmarshal(this.Buf[:pkgLen], &mes)
    54. if err != nil {
    55. return
    56. }
    57. return
    58. }

    server/processBlock/userProcess.go

    1. package processBlock
    2. import (
    3. "fmt"
    4. "net"
    5. "go_code/chatroom/common/message"
    6. "go_code/chatroom/server/utils"
    7. "encoding/json"
    8. )
    9. type UserProcess struct {
    10. Conn net.Conn
    11. }
    12. //编写一个函数serverProcessLogin函数,专门处理登录请求
    13. func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
    14. //先从mes中取出mes.Data,并直接反序列化成LoginMes
    15. var loginMes message.LoginMes
    16. err = json.Unmarshal([]byte(mes.Data), &loginMes)
    17. if err != nil {
    18. fmt.Printf("json.Unmarshal fail, err = %v\n", err)
    19. return
    20. }
    21. //1.声明一个resMes
    22. var resMes message.Message
    23. resMes.Type = message.LoginResMesType
    24. //2.再声明一个loginResMes,并完成赋值
    25. var loginResMes message.LoginResMes
    26. //如果用户id=100,密码=123456,则合法,不然,则不合法
    27. if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
    28. //合法
    29. loginResMes.Code = 200
    30. } else {
    31. //不合法
    32. loginResMes.Code = 500 //500 表示不存在
    33. loginResMes.Error = "该用户不存在,请注册后使用"
    34. }
    35. //3.将loginResMes序列化
    36. data, err := json.Marshal(loginResMes)
    37. if err != nil {
    38. fmt.Printf("json.Marshal fail, err = %v\n", err)
    39. return
    40. }
    41. //4.将data赋值给resMes
    42. resMes.Data = string(data)
    43. //5.对resMes序列化,准备发送
    44. data, err = json.Marshal(resMes)
    45. if err != nil {
    46. fmt.Printf("json.Marshal fail, err = %v\n", err)
    47. return
    48. }
    49. //6.发送data,将其封装到writePkg函数中
    50. //因为使用了分层模式(mvc),先创建Transfer实例,然后读取
    51. tf := &utils.Transfer{
    52. Conn: this.Conn,
    53. }
    54. err = tf.WritePkg(data)
    55. return
    56. }

    修改客户端,先画出程序框架图,再写代码

    client/main.go

    1. package main
    2. import (
    3. "fmt"
    4. "os"
    5. "go_code/chatroom/client/processBlock"
    6. )
    7. //定义两个全局变量,一个用户id,一个用户密码
    8. var userId int
    9. var userPwd string
    10. func main() {
    11. //接收用户的选择
    12. var key int
    13. for true {
    14. fmt.Println("-------------------欢迎登录多人聊天系统-------------------")
    15. fmt.Println("\t\t\t\t 1 登录聊天室")
    16. fmt.Println("\t\t\t\t 2 注册用户")
    17. fmt.Println("\t\t\t\t 3 退出系统")
    18. fmt.Println("\t\t\t\t 请选择(1~3):")
    19. fmt.Scanf("%d\n", &key)
    20. switch key {
    21. case 1 :
    22. fmt.Println("登录聊天室")
    23. //说明用户要登录
    24. fmt.Println("请输入用户的id:")
    25. fmt.Scanf("%d\n", &userId)
    26. fmt.Println("请输入用户的密码:")
    27. fmt.Scanf("%s\n", &userPwd)
    28. //先把登录的函数写到另一个文件,比如:login.go
    29. //因为使用了分层模式(mvc),故调用分层模式中processBlock.UserProcess.Login()处理
    30. up := &processBlock.UserProcess{}
    31. up.Login(userId, userPwd)
    32. case 2 :
    33. fmt.Println("注册用户")
    34. case 3 :
    35. fmt.Println("退出系统")
    36. os.Exit(0)
    37. default:
    38. fmt.Println("输入有误,请重新输入")
    39. }
    40. }
    41. }

    client/utils/utils.go

    1. package utils
    2. import (
    3. "fmt"
    4. "net"
    5. "go_code/chatroom/common/message"
    6. "encoding/binary"
    7. "encoding/json"
    8. )
    9. //将方法关联到结构体中
    10. type Transfer struct {
    11. Conn net.Conn
    12. Buf [8096]byte //传输时,使用的缓冲
    13. }
    14. func (this *Transfer) WritePkg(data []byte) (err error) {
    15. //先发送一个长度给对方
    16. //先把data的长度发送给对方
    17. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    18. var pkgLen uint32
    19. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    20. binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    21. //发送长度
    22. n, err := this.Conn.Write(this.Buf[:4])
    23. if err != nil || n != 4 {
    24. fmt.Printf(" conn write err = %v\n", err)
    25. return
    26. }
    27. //发送data本身
    28. n, err = this.Conn.Write(data)
    29. if err != nil || n != int(pkgLen) {
    30. fmt.Printf(" conn write err = %v\n", err)
    31. }
    32. return
    33. }
    34. //读取数据包直接封装成一个函数readPkg()
    35. func (this *Transfer) ReadPkg() (mes message.Message, err error) {
    36. fmt.Println("读取客户端发送的数据...")
    37. //conn.Read在conn没有被关闭的情况下,才会阻塞
    38. //如果客户端关闭了conn,则不会阻塞
    39. _, err = this.Conn.Read(this.Buf[:4])
    40. if err != nil {
    41. return
    42. }
    43. //根据buf[:4]转成一个uint32类型
    44. var pkgLen uint32
    45. pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
    46. //根据pkgLen读取消息内容
    47. n, err := this.Conn.Read(this.Buf[:pkgLen])
    48. if n != int(pkgLen) || err != nil {
    49. // err = errors.New("read pkg body error")
    50. return
    51. }
    52. //把pkgLen反序列化成message.Message
    53. err = json.Unmarshal(this.Buf[:pkgLen], &mes)
    54. if err != nil {
    55. return
    56. }
    57. return
    58. }

    client/processBlock/server.go

    1. package processBlock
    2. import (
    3. "fmt"
    4. "os"
    5. "net"
    6. "go_code/chatroom/client/utils"
    7. )
    8. //显示登录成功后的界面
    9. func ShowMenu() {
    10. fmt.Println("--------恭喜xxx登录成功--------")
    11. fmt.Println("--------1.显示在线用户列表--------")
    12. fmt.Println("--------2.发送消息--------")
    13. fmt.Println("--------3.信息列表--------")
    14. fmt.Println("--------4.退出系统--------")
    15. fmt.Println("-------请选择(1~4):----")
    16. var key int
    17. fmt.Scanf("%d\n", &key)
    18. switch key {
    19. case 1:
    20. fmt.Println("显示在线用户列表")
    21. case 2:
    22. fmt.Println("发送消息")
    23. case 3:
    24. fmt.Println("信息列表")
    25. case 4:
    26. fmt.Println("退出了系统")
    27. os.Exit(0)
    28. default:
    29. fmt.Println("输入错误,请重新输入")
    30. }
    31. }
    32. //和服务端端保持通讯
    33. func ServerProcessMes(conn net.Conn) {
    34. //创建一个Transfer实例,让它不停地读取服务器发送的消息
    35. tf := &utils.Transfer {
    36. Conn: conn,
    37. }
    38. for {
    39. fmt.Println("客户端正在等待读取服务器发送的消息")
    40. mes, err := tf.ReadPkg()
    41. if err != nil {
    42. fmt.Println("tf.readpkg err =", err)
    43. return
    44. }
    45. //如果读取到消息,则进行下一步逻辑处理
    46. fmt.Printf("mes=%v\n", mes)
    47. }
    48. }

    client/processBlock/userProcess.go

    1. package processBlock
    2. import (
    3. "fmt"
    4. "net"
    5. "encoding/binary"
    6. "encoding/json"
    7. "go_code/chatroom/common/message"
    8. "go_code/chatroom/client/utils"
    9. )
    10. type UserProcess struct {
    11. }
    12. //写一个函数, 完成登录
    13. func (this *UserProcess) Login(userId int, userPwd string) (err error) {
    14. // //下一步 开始定协议
    15. // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    16. // return
    17. //1.连接到服务器端
    18. conn, err := net.Dial("tcp", "127.0.0.1:8889")
    19. if err != nil {
    20. fmt.Printf("net dial err = %v\n", err)
    21. return
    22. }
    23. //延时关闭
    24. defer conn.Close()
    25. //2.准备通过conn发送消息给服务端
    26. var mes message.Message
    27. mes.Type = message.LoginMesType
    28. //3.创建一个LoginMes结构体
    29. var loginMes message.LoginMes
    30. loginMes.UserId = userId
    31. loginMes.UserPwd = userPwd
    32. //4.将loginMes序列化
    33. data, err := json.Marshal(loginMes)
    34. if err != nil {
    35. fmt.Printf("json marshal err = %v\n", err)
    36. return
    37. }
    38. //5.将序列化后的loginMes byte切片赋给mes.Data
    39. mes.Data = string(data)
    40. //6.将mes序列化
    41. data, err = json.Marshal(mes)
    42. if err != nil {
    43. fmt.Printf("json marshal err = %v\n", err)
    44. return
    45. }
    46. //7.这个时候data就是要发送的消息
    47. //7.1先把data的长度发送给服务器
    48. //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    49. var pkgLen uint32
    50. pkgLen = uint32(len(data)) //把data的长度转换成uint32,供后续转成byte切片长度使用
    51. var buf [4]byte
    52. binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    53. //发送长度
    54. n, err := conn.Write(buf[:4])
    55. if err != nil || n != 4 {
    56. fmt.Printf(" conn write err = %v\n", err)
    57. return
    58. }
    59. // fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data))
    60. //发送消息本身
    61. _, err = conn.Write(data)
    62. if err != nil {
    63. fmt.Printf(" conn write err = %v\n", err)
    64. return
    65. }
    66. //休眠20s
    67. // time.Sleep(20 * time.Second)
    68. // fmt.Println("休眠20s...")
    69. //处理服务端返回的消息
    70. //创建一个Transfer实例
    71. tf := &utils.Transfer{
    72. Conn: conn,
    73. }
    74. mes, err = tf.ReadPkg()
    75. if err != nil {
    76. fmt.Println("readPkg(conn) err = ", err)
    77. return
    78. }
    79. //将mes中的Data反序列化成LoginResMes
    80. var loginResMes message.LoginResMes
    81. err = json.Unmarshal([]byte(mes.Data), &loginResMes)
    82. if err != nil {
    83. fmt.Println("json.Unmarshal err = ", err)
    84. return
    85. }
    86. if loginResMes.Code == 200 {
    87. // fmt.Println("登录成功")
    88. //这里需要启动一个协程,保持和服务器通讯,如果服务器有数据,则推送给客户端,客户端接收后显示在终端
    89. go ServerProcessMes(conn)
    90. //1.显示登录成功的菜单[循环显示]
    91. for {
    92. ShowMenu()
    93. }
    94. } else if loginResMes.Code == 500 {
    95. fmt.Println(loginResMes.Error)
    96. }
    97. return
    98. }

    [上一节][go学习笔记.第十六章.TCP编程] 1.基本介绍以及入门案例

    [下一节][go学习笔记.第十六章.TCP编程] 3.项目-海量用户即时通讯系统-redis介入,用户登录,注册 

  • 相关阅读:
    数据结构(Java):力扣&牛客 二叉树面试OJ题
    Java - 由ReflectionFactory引发的对final关键字的思考
    网盘项目分析
    复制远程连接到Linux使用VIM打开的内容到Windows
    Android通过JNI操作GPIO
    vue3中使用vue-i18n(ts中使用$t, vue3不用this)
    基于python的数据结构与算法——线性表
    共同富裕-三大维度-各省份、城市、农村基尼系数-附带多种计算方法
    CENTURY模型应用
    如何在vector中插入和删除元素?
  • 原文地址:https://blog.csdn.net/zhoupenghui168/article/details/127908847