1、网络

1. 分层

2. socket

3. http

编写web的语言:

  1. java
  2. php ==> 现在用go重写
  3. python ,豆瓣
  4. go ===> beego,gin主流的web框架

https协议:浏览器发送的就是http请求

  1. http是应用层的协议,底层还是依赖传输层:tcp(短连接),网络层(ip)
  2. 无状态的,每次请求都是独立的,下次请求需要重新建立连接
  3. https:
    1. http是标准协议,明文传输,不安全
    2. https不是标准协议,https:http + ssl(非对称加密,数字证书)
    3. 现在所有网站都会尽量要求使用https开发:安全

1、http的请求报文格式

Go-网络 - 图1

一个http可以分为4部分:

  1. 请求行:包含3部分
    1. 格式:方法 + URL + 协议版本号
    2. 请求方法
      1. GET:获取数据
      2. POST:上传数据(表单格式,json格式)
      3. PUT:修改数据
      4. DELETE:删除数据
  2. 请求头
    1. 格式:key :value
    2. 可以包含多个键值对(包含协议自带,也包含用户自定义的)
    3. 常见重要的头:
      1. Accept:接收数据格式
      2. User-agent:描述用户浏览器信息
      3. Connection:Keep-Alive(长链接),Close(短连接)
      4. Accept-Encoding:gzip.. 描述可以接收的编码
      5. Cookie:由服务器设置的key=value数据,客户端下次请求的时候可以携带过来
      6. Content-Type:application/-from(表示上传的是表单);application/json(表示body是json格式的)
      7. 用户自定义的:
        1. name:Duke
        2. age:18
  3. 空行
    1. 告诉服务器请求头结束了,用于分割
  4. 请求包体(可选的)
    1. 一般在post方法时会配套提供请求包体
    2. 在GET的时候也可以提供BODY,但是容易制造混淆(不建议)
    3. 上传两种格式:
      1. 表单:姓名、性别、年龄
      2. json格式数据

2. http-client

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net/http"
  6. )
  7. func main() {
  8. //http包
  9. client := http.Client{}
  10. response, err := client.Get("https://www.baidu.com")
  11. if err != nil {
  12. fmt.Println("client.Get err:", err)
  13. }
  14. //获取响应体 - 内容很多放在最上面
  15. body := response.Body
  16. readBodyAll, err := ioutil.ReadAll(body)
  17. if err != nil {
  18. fmt.Println("body string:", err)
  19. }
  20. fmt.Println(readBodyAll)
  21. //获取响应请求头信息
  22. ct := response.Header.Get("Content-Type")
  23. date := response.Header.Get("Date")
  24. server := response.Header.Get("Server")
  25. fmt.Println("ct:", ct)
  26. fmt.Println("date:", date)
  27. fmt.Println("server:", server)
  28. url := response.Request.URL
  29. code := response.StatusCode
  30. status := response.Status
  31. fmt.Println("url:", url)
  32. fmt.Println("code:", code)
  33. fmt.Println("status:", status)
  34. }

3. http-server

  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. )
  7. func main() {
  8. //注册路由:匹配不通的信息处理不同的逻辑
  9. //xxx/user ===> func1
  10. //xxx/name ===> func2
  11. //xxx/id ===> func3
  12. //func()是回调函数,用于路由的响应,原型是固定的
  13. //https://127.0.0.1:8080/user
  14. http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) {
  15. //request:包含客户端发送来的数据
  16. fmt.Println("用户请求详情:", request)
  17. //这里是具体的处理业务逻辑
  18. //writer:通过writer将数据返回
  19. _, _ = io.WriteString(writer, "这是/user请求返回的数据\n")
  20. })
  21. //https://127.0.0.1:8080/name
  22. http.HandleFunc("/name", func(writer http.ResponseWriter, request *http.Request) {
  23. _, _ = io.WriteString(writer, "这是/name请求返回的数据\n")
  24. })
  25. //https://127.0.0.1:8080/id
  26. http.HandleFunc("/id", func(writer http.ResponseWriter, request *http.Request) {
  27. _, _ = io.WriteString(writer, "这是/id请求返回的数据\n")
  28. })
  29. fmt.Println("Server start ...")
  30. if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
  31. fmt.Println("http.ListentAndServer err:", err)
  32. return
  33. }
  34. }

4. json

json编码解码

在网络传输的时候,把结构体,编码成json字符串:传输 ==> 结构体 ===> 字符串 ===> 编码

在对端接收字符串,需要将字符串转换成结构体,然后操作 ==> 字符串 ==> 结构体 ==> 解码

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. //结构体
  7. type Student struct {
  8. Id int
  9. Name string
  10. Age int
  11. gender string //注意小写,小写字母开头的,在json编码解码时会被忽略
  12. }
  13. func main() {
  14. // 在网络传输的时候,把结构体,编码成json字符串:传输 ==> 结构体 ===> 字符串 ===> 编码
  15. // 在对端接收字符串,需要将字符串转换成结构体,然后操作 ==> 字符串 ==> 结构体 ==> 解码
  16. lily := Student{
  17. Id: 1,
  18. Name: "lily",
  19. Age: 21,
  20. gender: "女",
  21. }
  22. //编码:序列化 ,结构体==> 字符串
  23. encodeInfo, err := json.Marshal(&lily)
  24. if err != nil {
  25. fmt.Println("json.Marshal err", err)
  26. return
  27. }
  28. fmt.Println("encodeInfo:", string(encodeInfo))
  29. //在对端解码:
  30. var lily2 Student
  31. if err := json.Unmarshal([]byte(encodeInfo), &lily2); err != nil {
  32. fmt.Println("json.Unmarshal err:", err)
  33. return
  34. }
  35. fmt.Println("name:", lily2.Name)
  36. fmt.Println("id:", lily2.Id)
  37. fmt.Println("age:", lily2.Age)
  38. fmt.Println("gender:", lily2.gender)
  39. }
  • 由于gender字段在结构体中是小写,所以被忽略了

Go-网络 - 图2

结构体标签

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. //结构体
  7. type Teacher struct {
  8. Id int
  9. Name string `json:"-"` //==> 在使用json编码时,这个字段不参与编码
  10. Subject string `json:"Sub_name"` //==> 别名
  11. Age int `json:"age,string"` //==> 别名,并转换数据类型(一定要两个字段:名字,数据类型)
  12. Address string `json:"address,omitempty"` //==>如果这个字段是空的,那么忽略掉,不参与编码
  13. gender string //注意小写,小写字母开头的,在json编码解码时会被忽略
  14. }
  15. func main() {
  16. t1 := Teacher{
  17. Id: 1,
  18. Name: "lily",
  19. Subject: "Go语言",
  20. Age: 21,
  21. gender: "男",
  22. // Address: "兰州",
  23. }
  24. fmt.Println("ti:", t1)
  25. encodeInfo, _ := json.Marshal(&t1)
  26. fmt.Println("encodeInfo:", string(encodeInfo))
  27. }

Go-网络 - 图3

总结:

  1. 编码时,字段首字母必须大写,否则无法编码(被忽略)
  2. 如果json格式要求key小写,可以通过标签(tag)来解决(别名)
  3. tag细节
  1. Name string `json:"-"` //==> 在使用json编码时,这个字段不参与编码
  2. Subject string `json:"Sub_name"` //==> 别名
  3. Age int `json:"age,string"` //==> 别名,并转换数据类型(一定要两个字段:名字,数据类型)
  4. Address string `json:"address,omitempty"` //==>如果这个字段是空的,那么忽略掉,不参与编码
  5. gender string //注意小写,小写字母开头的,在json编码解码时会被忽略

4. 聊天室-模拟

4.1 概述

  • 实现一个网络聊天室(群):功能分析
    • 上线下线
    • 聊天,其他人都可以看到消息
    • 查看当前聊天室用户名字
    • 可以修改自己的名字
    • 超时提出(潜水)

技术点分析:

  1. socket tcp编程 - 建立多个连接
  2. map结构
    1. 存储所有用户
    2. map遍历
    3. map删除
  3. go程、channel
  4. select 监听(超时退出,主动退出)
  5. timer 定时器

4.2 实现思路

阶段一:

  1. 思路分析
    1. tcp socket:建立多个连接
  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. )
  6. //
  7. func main() {
  8. //创建服务器
  9. listen, err := net.Listen("tcp", ":8000")
  10. if err != nil {
  11. fmt.Println("net.Listen err:", err)
  12. return
  13. }
  14. fmt.Println("服务器启动成功,监听中...")
  15. for {
  16. fmt.Println("主go程监听中...")
  17. //监听
  18. conn, err := listen.Accept()
  19. if err != nil {
  20. fmt.Println("listen.Accepet err:", err)
  21. return
  22. }
  23. //建立连接
  24. fmt.Println("建立连接成功...")
  25. //启动处理业务go程
  26. go handler(conn)
  27. }
  28. }
  29. //处理具体业务
  30. func handler(conn net.Conn) {
  31. for {
  32. fmt.Println("业务go程监听中...")
  33. //TODO //代码这里以后再具体实现,当前保留
  34. buf := make([]byte, 1024)
  35. //读取客户端发送过来的请求数据
  36. cnt, err := conn.Read(buf)
  37. if err != nil {
  38. fmt.Println("conn.Read err:", err)
  39. return
  40. }
  41. //cnt-1:去掉最后的回车
  42. fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
  43. }
  44. }

Go-网络 - 图4

  1. 分析

Go-网络 - 图5

数据流向:

Go-网络 - 图6

  1. 定义User结构
  1. //定义User
  2. type User struct {
  3. //唯一Id
  4. id string
  5. name string
  6. //管道
  7. msg chan string
  8. }
  9. //创建全局的map结构,用于保存所有的用户
  10. var allUsers = make(map[string]User)

在handler中调用

  1. //TODO //代码这里以后再具体实现,当前保留
  2. clientAddr := conn.RemoteAddr().String()
  3. //创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
  4. newUser := User{
  5. name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
  6. id: clientAddr, //id,我们不会修改,作为map中key
  7. msg: make(chan string, 10), //注意分配空间,否则无法写入数据
  8. }
  9. //添加User 到map结构
  10. allUsers[newUser.id] = newUser

Go-网络 - 图7

  1. 定义message通道
  1. //定义massage全局通道,接收所有用户发送的数据
  2. var message = make(chan string, 10)
  3. //向所有用户广播消息,启动一个全局唯一的go程
  4. func broadcast() {
  5. fmt.Println("广播go程启动成功...")
  6. //1. 从message读取数据
  7. info := <-message
  8. //将数据写入每一个用户的msg管道
  9. for _, user := range allUsers {
  10. user.msg <- info
  11. }
  12. }

上线通知:

Go-网络 - 图8

bug修复-for循环

  1. //向所有用户广播消息,启动一个全局唯一的go程
  2. func broadcast() {
  3. fmt.Println("广播go程启动成功...")
  4. defer fmt.Println("broadcast程序退出!")
  5. for {
  6. fmt.Println("broadcast监听message中...")
  7. //1. 从message读取数据
  8. info := <-message
  9. //将数据写入每一个用户的msg管道
  10. for _, user := range allUsers {
  11. //如果msg是非缓冲的,将会阻塞
  12. user.msg <- info
  13. }
  14. }
  15. }
  1. User监听通道go程

每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端

  1. //完整逻辑代码
  2. package main
  3. import (
  4. "fmt"
  5. "net"
  6. )
  7. //定义User
  8. type User struct {
  9. //唯一Id
  10. id string
  11. name string
  12. //管道
  13. msg chan string
  14. }
  15. //创建全局的map结构,用于保存所有的用户
  16. var allUsers = make(map[string]User)
  17. //定义massage全局通道,接收所有用户发送的数据
  18. var message = make(chan string, 10)
  19. func main() {
  20. //创建服务器
  21. listen, err := net.Listen("tcp", ":8000")
  22. if err != nil {
  23. fmt.Println("net.Listen err:", err)
  24. return
  25. }
  26. fmt.Println("服务器启动成功,监听中...")
  27. //启动全局唯一的go程,负责监听message通道,写给所有的用户
  28. go broadcast()
  29. for {
  30. fmt.Println("主go程监听中...")
  31. //监听
  32. conn, err := listen.Accept()
  33. if err != nil {
  34. fmt.Println("listen.Accepet err:", err)
  35. return
  36. }
  37. //建立连接
  38. fmt.Println("建立连接成功...")
  39. //启动处理业务go程
  40. go handler(conn)
  41. }
  42. }
  43. //处理具体业务
  44. func handler(conn net.Conn) {
  45. fmt.Println("业务go程监听中...")
  46. //TODO //代码这里以后再具体实现,当前保留
  47. clientAddr := conn.RemoteAddr().String()
  48. //创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
  49. newUser := User{
  50. name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
  51. id: clientAddr, //id,我们不会修改,作为map中key
  52. msg: make(chan string, 10), //注意分配空间,否则无法写入数据
  53. }
  54. //添加User 到map结构
  55. allUsers[newUser.id] = newUser
  56. //启动go程,将msg返回给客户端
  57. go writeBackToClient(&newUser, conn)
  58. //向message写入通知消息,广播当前用户上线通知
  59. loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!", newUser.id, newUser.name)
  60. message <- loginInfo
  61. fmt.Println("message接收到的消息是:", loginInfo)
  62. for {
  63. //具体业务逻辑
  64. buf := make([]byte, 1024)
  65. //读取客户端发送过来的请求数据
  66. cnt, err := conn.Read(buf)
  67. if err != nil {
  68. fmt.Println("conn.Read err:", err)
  69. return
  70. }
  71. //cnt-1:去掉最后的回车
  72. fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
  73. }
  74. }
  75. //每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
  76. func writeBackToClient(user *User, conn net.Conn) {
  77. //TODO
  78. fmt.Printf("User:%s的go程正在监听自己的msg管道\n", user.name)
  79. for data := range user.msg {
  80. fmt.Printf("user:%s 写回给客户端数据为:%s\n", user.name, data)
  81. _, _ = conn.Write([]byte(data))
  82. }
  83. }
  84. //向所有用户广播消息,启动一个全局唯一的go程
  85. func broadcast() {
  86. fmt.Println("广播go程启动成功...")
  87. defer fmt.Println("broadcast程序退出!")
  88. for {
  89. fmt.Println("broadcast监听message中...")
  90. //1. 从message读取数据
  91. info := <-message
  92. //将数据写入每一个用户的msg管道
  93. for _, user := range allUsers {
  94. //如果msg是非缓冲的,将会阻塞
  95. user.msg <- info
  96. }
  97. }
  98. }

4.3 功能

1、查询用户
  • who:—> 将当前所有的用户id,name返回给当前用户(遍历map)
2、重命名
  • 规则:rename | Duke
    • 读取数据判断长度大于8,判断字符是rename
    • 使用 | 分割,获取 | 后面的作为名字
    • 更新用户名字newUser.name = Duke
    • 通知客户端更新成功
3、主动退出
  • 用户退出:清理工作(输入:\quit、ctrl+c)
    • 从map中删除
    • 对应的conn要close
4、超时退出

定时器进行超时管理

60s内没有发送任何数据,连接关闭

  1. time.After(60 * time.Second) //chan time

5、map读写上锁(bug)

  • map不允许同时读写
  1. var lock sync.RWMutex
  2. lock.Lock()
  3. lock.Unlock()

6、全部代码

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "strings"
  6. "time"
  7. )
  8. //定义User
  9. type User struct {
  10. //唯一Id
  11. id string
  12. name string
  13. //管道
  14. msg chan string
  15. }
  16. //创建全局的map结构,用于保存所有的用户
  17. var allUsers = make(map[string]User)
  18. //定义massage全局通道,接收所有用户发送的数据
  19. var message = make(chan string, 10)
  20. func main() {
  21. //创建服务器
  22. listen, err := net.Listen("tcp", ":8000")
  23. if err != nil {
  24. fmt.Println("net.Listen err:", err)
  25. return
  26. }
  27. fmt.Println("服务器启动成功,监听中...")
  28. //启动全局唯一的go程,负责监听message通道,写给所有的用户
  29. go broadcast()
  30. for {
  31. fmt.Println("主go程监听中...")
  32. //监听
  33. conn, err := listen.Accept()
  34. if err != nil {
  35. fmt.Println("listen.Accepet err:", err)
  36. return
  37. }
  38. //建立连接
  39. fmt.Println("建立连接成功...")
  40. //启动处理业务go程
  41. go handler(conn)
  42. }
  43. }
  44. //处理具体业务
  45. func handler(conn net.Conn) {
  46. fmt.Println("业务go程监听中...")
  47. //TODO //代码这里以后再具体实现,当前保留
  48. clientAddr := conn.RemoteAddr().String()
  49. //创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
  50. newUser := User{
  51. name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
  52. id: clientAddr, //id,我们不会修改,作为map中key
  53. msg: make(chan string, 10), //注意分配空间,否则无法写入数据
  54. }
  55. //添加User 到map结构
  56. allUsers[newUser.id] = newUser
  57. //启动go程,将msg返回给客户端
  58. go writeBackToClient(&newUser, conn)
  59. //向message写入通知消息,广播当前用户上线通知
  60. loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!\n", newUser.id, newUser.name)
  61. message <- loginInfo
  62. fmt.Println("message接收到的消息是:", loginInfo)
  63. //创建一个用于重置连接计数器的管道,用于告知watch函数,当前用户正在输入
  64. var resTimer = make(chan bool)
  65. //定义退出信号,用于监听client退出
  66. var isQuit = make(chan bool)
  67. //启动go程负责监听退出信号
  68. go watch(&newUser, conn, isQuit, resTimer)
  69. for {
  70. //具体业务逻辑
  71. buf := make([]byte, 1024)
  72. //读取客户端发送过来的请求数据
  73. cnt, err := conn.Read(buf)
  74. if cnt == 0 {
  75. fmt.Println("客户端ctrl+c,准备退出...")
  76. //清理,map删除用户、conn close
  77. //服务器还可以主动退出
  78. //在这里不进行真正的退出,而是发送一个退出信号,统一做退出处理,可以使用新的管道做信号传递
  79. isQuit <- true
  80. }
  81. if err != nil {
  82. fmt.Println("conn.Read err:", err)
  83. return
  84. }
  85. //cnt-1:去掉最后的回车
  86. fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
  87. //----------业务逻辑代码 开始----------
  88. //1. 查询当前所有用户:who命令
  89. // a. 判断接收的数据是不是who ===> 长度&&字符串
  90. userInput := buf[:cnt-1] //这是用户输入的数据,去掉回车
  91. if len(userInput) == 4 && string(userInput) == "\\who" {
  92. // b. 遍历allUsers这个map:(key := user.id value := user本身),将id和name拼接成一个字符串,返回给客户端
  93. fmt.Println("用户查询所有用户:")
  94. //创建切面用于包含所有的用户信息,以便返回给客户查询(切面服务直接返回的)
  95. var userInfos []string
  96. for _, user := range allUsers {
  97. userInfo := fmt.Sprintf("userId: %s, userName: %s\n", user.id, user.name)
  98. userInfos = append(userInfos, userInfo)
  99. }
  100. //最终写入管道中,一定是一个字符串
  101. r := strings.Join(userInfos, "\n")
  102. //将数据返回给查询的客户端
  103. newUser.msg <- r
  104. } else if len(userInput) > 9 && string(userInput[:7]) == "\\rename" {
  105. // ● 规则:rename | Duke
  106. // ○ 读取数据判断长度大于8,判断字符是rename
  107. // ○ 使用 | 分割,获取 | 后面的作为名字
  108. fmt.Println("用户修改名称")
  109. // ○ 更新用户名字newUser.name = Duke
  110. // array := strings.Split(string(userInput), "|")
  111. // name := array[1]
  112. newUser.name = strings.Split(string(userInput), "|")[1]
  113. //更新map中的user
  114. allUsers[newUser.id] = newUser
  115. fmt.Println("用户更新名称后:", allUsers)
  116. // ○ 通知客户端更新成功
  117. newUser.msg <- "rename successfuly" + "\n"
  118. } else {
  119. //如果不是命令,那么只需要写入广播通道即可,由其他go程进行常规转发
  120. message <- string(userInput)
  121. }
  122. resTimer <- true
  123. //----------业务逻辑代码 结束----------
  124. }
  125. }
  126. //每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
  127. func writeBackToClient(user *User, conn net.Conn) {
  128. //TODO
  129. fmt.Printf("User:%s的go程正在监听自己的msg管道\n", user.name)
  130. for data := range user.msg {
  131. fmt.Printf("user:%s 写回给客户端数据为:\n%s \n", user.name, data)
  132. _, _ = conn.Write([]byte(data))
  133. }
  134. }
  135. //向所有用户广播消息,启动一个全局唯一的go程
  136. func broadcast() {
  137. fmt.Println("广播go程启动成功...")
  138. defer fmt.Println("broadcast程序退出!")
  139. for {
  140. fmt.Println("broadcast监听message中...")
  141. //1. 从message读取数据
  142. info := <-message
  143. //将数据写入每一个用户的msg管道
  144. for _, user := range allUsers {
  145. //如果msg是非缓冲的,将会阻塞
  146. user.msg <- info + "\n" //返回客户端后光标换行
  147. }
  148. }
  149. }
  150. //启动另一个go程,负责监听退出信号,触发后,进行清理工作:delete map,conn close都在这里处理
  151. func watch(user *User, conn net.Conn, isQuit, resTimer <-chan bool) {
  152. fmt.Println("启动监听退出信号go程...")
  153. defer fmt.Println("watch退出了...")
  154. for {
  155. select {
  156. case <-isQuit:
  157. logoutInfo := fmt.Sprintf("%s exit already!(主动) \n", user.name)
  158. fmt.Println("删除当前用户(主动):", user.name)
  159. delete(allUsers, user.id)
  160. message <- logoutInfo
  161. conn.Close()
  162. return
  163. case <-time.After(5 * time.Second): //超过指定时间退出
  164. logoutInfo := fmt.Sprintf("%s timeout exit already!(超时) \n", user.name)
  165. fmt.Println("删除当前用户(超时):", user.name)
  166. delete(allUsers, user.id)
  167. message <- logoutInfo
  168. conn.Close()
  169. return
  170. case <-resTimer:
  171. fmt.Printf("连接%s重置计数器...\n", user.name)
  172. }
  173. }
  174. }