1、网络
1. 分层
2. socket
3. http
编写web的语言:
- java
- php ==> 现在用go重写
- python ,豆瓣
- go ===> beego,gin主流的web框架
https协议:浏览器发送的就是http请求
- http是应用层的协议,底层还是依赖传输层:tcp(短连接),网络层(ip)
- 无状态的,每次请求都是独立的,下次请求需要重新建立连接
- https:
- http是标准协议,明文传输,不安全
- https不是标准协议,https:http + ssl(非对称加密,数字证书)
- 现在所有网站都会尽量要求使用https开发:安全
1、http的请求报文格式
一个http可以分为4部分:
- 请求行:包含3部分
- 格式:方法 + URL + 协议版本号
- 请求方法
- GET:获取数据
- POST:上传数据(表单格式,json格式)
- PUT:修改数据
- DELETE:删除数据
- 请求头
- 格式:key :value
- 可以包含多个键值对(包含协议自带,也包含用户自定义的)
- 常见重要的头:
- Accept:接收数据格式
- User-agent:描述用户浏览器信息
- Connection:Keep-Alive(长链接),Close(短连接)
- Accept-Encoding:gzip.. 描述可以接收的编码
- Cookie:由服务器设置的key=value数据,客户端下次请求的时候可以携带过来
- Content-Type:application/-from(表示上传的是表单);application/json(表示body是json格式的)
- 用户自定义的:
- name:Duke
- age:18
- 空行
- 告诉服务器请求头结束了,用于分割
- 请求包体(可选的)
- 一般在post方法时会配套提供请求包体
- 在GET的时候也可以提供BODY,但是容易制造混淆(不建议)
- 上传两种格式:
- 表单:姓名、性别、年龄
- json格式数据
2. http-client
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
//http包
client := http.Client{}
response, err := client.Get("https://www.baidu.com")
if err != nil {
fmt.Println("client.Get err:", err)
}
//获取响应体 - 内容很多放在最上面
body := response.Body
readBodyAll, err := ioutil.ReadAll(body)
if err != nil {
fmt.Println("body string:", err)
}
fmt.Println(readBodyAll)
//获取响应请求头信息
ct := response.Header.Get("Content-Type")
date := response.Header.Get("Date")
server := response.Header.Get("Server")
fmt.Println("ct:", ct)
fmt.Println("date:", date)
fmt.Println("server:", server)
url := response.Request.URL
code := response.StatusCode
status := response.Status
fmt.Println("url:", url)
fmt.Println("code:", code)
fmt.Println("status:", status)
}
3. http-server
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//注册路由:匹配不通的信息处理不同的逻辑
//xxx/user ===> func1
//xxx/name ===> func2
//xxx/id ===> func3
//func()是回调函数,用于路由的响应,原型是固定的
//https://127.0.0.1:8080/user
http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) {
//request:包含客户端发送来的数据
fmt.Println("用户请求详情:", request)
//这里是具体的处理业务逻辑
//writer:通过writer将数据返回
_, _ = io.WriteString(writer, "这是/user请求返回的数据\n")
})
//https://127.0.0.1:8080/name
http.HandleFunc("/name", func(writer http.ResponseWriter, request *http.Request) {
_, _ = io.WriteString(writer, "这是/name请求返回的数据\n")
})
//https://127.0.0.1:8080/id
http.HandleFunc("/id", func(writer http.ResponseWriter, request *http.Request) {
_, _ = io.WriteString(writer, "这是/id请求返回的数据\n")
})
fmt.Println("Server start ...")
if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
fmt.Println("http.ListentAndServer err:", err)
return
}
}
4. json
json编码解码
在网络传输的时候,把结构体,编码成json字符串:传输 ==> 结构体 ===> 字符串 ===> 编码
在对端接收字符串,需要将字符串转换成结构体,然后操作 ==> 字符串 ==> 结构体 ==> 解码
package main
import (
"encoding/json"
"fmt"
)
//结构体
type Student struct {
Id int
Name string
Age int
gender string //注意小写,小写字母开头的,在json编码解码时会被忽略
}
func main() {
// 在网络传输的时候,把结构体,编码成json字符串:传输 ==> 结构体 ===> 字符串 ===> 编码
// 在对端接收字符串,需要将字符串转换成结构体,然后操作 ==> 字符串 ==> 结构体 ==> 解码
lily := Student{
Id: 1,
Name: "lily",
Age: 21,
gender: "女",
}
//编码:序列化 ,结构体==> 字符串
encodeInfo, err := json.Marshal(&lily)
if err != nil {
fmt.Println("json.Marshal err", err)
return
}
fmt.Println("encodeInfo:", string(encodeInfo))
//在对端解码:
var lily2 Student
if err := json.Unmarshal([]byte(encodeInfo), &lily2); err != nil {
fmt.Println("json.Unmarshal err:", err)
return
}
fmt.Println("name:", lily2.Name)
fmt.Println("id:", lily2.Id)
fmt.Println("age:", lily2.Age)
fmt.Println("gender:", lily2.gender)
}
- 由于gender字段在结构体中是小写,所以被忽略了
结构体标签
package main
import (
"encoding/json"
"fmt"
)
//结构体
type Teacher struct {
Id int
Name string `json:"-"` //==> 在使用json编码时,这个字段不参与编码
Subject string `json:"Sub_name"` //==> 别名
Age int `json:"age,string"` //==> 别名,并转换数据类型(一定要两个字段:名字,数据类型)
Address string `json:"address,omitempty"` //==>如果这个字段是空的,那么忽略掉,不参与编码
gender string //注意小写,小写字母开头的,在json编码解码时会被忽略
}
func main() {
t1 := Teacher{
Id: 1,
Name: "lily",
Subject: "Go语言",
Age: 21,
gender: "男",
// Address: "兰州",
}
fmt.Println("ti:", t1)
encodeInfo, _ := json.Marshal(&t1)
fmt.Println("encodeInfo:", string(encodeInfo))
}
总结:
- 编码时,字段首字母必须大写,否则无法编码(被忽略)
- 如果json格式要求key小写,可以通过标签(tag)来解决(别名)
- tag细节
Name string `json:"-"` //==> 在使用json编码时,这个字段不参与编码
Subject string `json:"Sub_name"` //==> 别名
Age int `json:"age,string"` //==> 别名,并转换数据类型(一定要两个字段:名字,数据类型)
Address string `json:"address,omitempty"` //==>如果这个字段是空的,那么忽略掉,不参与编码
gender string //注意小写,小写字母开头的,在json编码解码时会被忽略
4. 聊天室-模拟
4.1 概述
- 实现一个网络聊天室(群):功能分析
- 上线下线
- 聊天,其他人都可以看到消息
- 查看当前聊天室用户名字
- 可以修改自己的名字
- 超时提出(潜水)
技术点分析:
- socket tcp编程 - 建立多个连接
- map结构
- 存储所有用户
- map遍历
- map删除
- go程、channel
- select 监听(超时退出,主动退出)
- timer 定时器
4.2 实现思路
阶段一:
- 思路分析
- tcp socket:建立多个连接
package main
import (
"fmt"
"net"
)
//
func main() {
//创建服务器
listen, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
fmt.Println("服务器启动成功,监听中...")
for {
fmt.Println("主go程监听中...")
//监听
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accepet err:", err)
return
}
//建立连接
fmt.Println("建立连接成功...")
//启动处理业务go程
go handler(conn)
}
}
//处理具体业务
func handler(conn net.Conn) {
for {
fmt.Println("业务go程监听中...")
//TODO //代码这里以后再具体实现,当前保留
buf := make([]byte, 1024)
//读取客户端发送过来的请求数据
cnt, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
//cnt-1:去掉最后的回车
fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
}
}
- 分析
数据流向:
- 定义User结构
//定义User
type User struct {
//唯一Id
id string
name string
//管道
msg chan string
}
//创建全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)
在handler中调用
//TODO //代码这里以后再具体实现,当前保留
clientAddr := conn.RemoteAddr().String()
//创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
newUser := User{
name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
id: clientAddr, //id,我们不会修改,作为map中key
msg: make(chan string, 10), //注意分配空间,否则无法写入数据
}
//添加User 到map结构
allUsers[newUser.id] = newUser
- 定义message通道
//定义massage全局通道,接收所有用户发送的数据
var message = make(chan string, 10)
//向所有用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")
//1. 从message读取数据
info := <-message
//将数据写入每一个用户的msg管道
for _, user := range allUsers {
user.msg <- info
}
}
上线通知:
bug修复-for循环
//向所有用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast程序退出!")
for {
fmt.Println("broadcast监听message中...")
//1. 从message读取数据
info := <-message
//将数据写入每一个用户的msg管道
for _, user := range allUsers {
//如果msg是非缓冲的,将会阻塞
user.msg <- info
}
}
}
- User监听通道go程
每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
//完整逻辑代码
package main
import (
"fmt"
"net"
)
//定义User
type User struct {
//唯一Id
id string
name string
//管道
msg chan string
}
//创建全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)
//定义massage全局通道,接收所有用户发送的数据
var message = make(chan string, 10)
func main() {
//创建服务器
listen, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
fmt.Println("服务器启动成功,监听中...")
//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()
for {
fmt.Println("主go程监听中...")
//监听
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accepet err:", err)
return
}
//建立连接
fmt.Println("建立连接成功...")
//启动处理业务go程
go handler(conn)
}
}
//处理具体业务
func handler(conn net.Conn) {
fmt.Println("业务go程监听中...")
//TODO //代码这里以后再具体实现,当前保留
clientAddr := conn.RemoteAddr().String()
//创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
newUser := User{
name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
id: clientAddr, //id,我们不会修改,作为map中key
msg: make(chan string, 10), //注意分配空间,否则无法写入数据
}
//添加User 到map结构
allUsers[newUser.id] = newUser
//启动go程,将msg返回给客户端
go writeBackToClient(&newUser, conn)
//向message写入通知消息,广播当前用户上线通知
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!", newUser.id, newUser.name)
message <- loginInfo
fmt.Println("message接收到的消息是:", loginInfo)
for {
//具体业务逻辑
buf := make([]byte, 1024)
//读取客户端发送过来的请求数据
cnt, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
//cnt-1:去掉最后的回车
fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
}
}
//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User, conn net.Conn) {
//TODO
fmt.Printf("User:%s的go程正在监听自己的msg管道\n", user.name)
for data := range user.msg {
fmt.Printf("user:%s 写回给客户端数据为:%s\n", user.name, data)
_, _ = conn.Write([]byte(data))
}
}
//向所有用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast程序退出!")
for {
fmt.Println("broadcast监听message中...")
//1. 从message读取数据
info := <-message
//将数据写入每一个用户的msg管道
for _, user := range allUsers {
//如果msg是非缓冲的,将会阻塞
user.msg <- info
}
}
}
4.3 功能
1、查询用户
- who:—> 将当前所有的用户id,name返回给当前用户(遍历map)
2、重命名
- 规则:rename | Duke
- 读取数据判断长度大于8,判断字符是rename
- 使用 | 分割,获取 | 后面的作为名字
- 更新用户名字newUser.name = Duke
- 通知客户端更新成功
3、主动退出
- 用户退出:清理工作(输入:\quit、ctrl+c)
- 从map中删除
- 对应的conn要close
4、超时退出
定时器进行超时管理
60s内没有发送任何数据,连接关闭
time.After(60 * time.Second) //chan time
5、map读写上锁(bug)
- map不允许同时读写
var lock sync.RWMutex
lock.Lock()
lock.Unlock()
6、全部代码
package main
import (
"fmt"
"net"
"strings"
"time"
)
//定义User
type User struct {
//唯一Id
id string
name string
//管道
msg chan string
}
//创建全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)
//定义massage全局通道,接收所有用户发送的数据
var message = make(chan string, 10)
func main() {
//创建服务器
listen, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
fmt.Println("服务器启动成功,监听中...")
//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()
for {
fmt.Println("主go程监听中...")
//监听
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accepet err:", err)
return
}
//建立连接
fmt.Println("建立连接成功...")
//启动处理业务go程
go handler(conn)
}
}
//处理具体业务
func handler(conn net.Conn) {
fmt.Println("业务go程监听中...")
//TODO //代码这里以后再具体实现,当前保留
clientAddr := conn.RemoteAddr().String()
//创建User:客户端和服务器建立连接时,会有ip和port ==> 当成User的id
newUser := User{
name: clientAddr, //可以修改,会提供rename修改,简历连接时,初始值和id相同
id: clientAddr, //id,我们不会修改,作为map中key
msg: make(chan string, 10), //注意分配空间,否则无法写入数据
}
//添加User 到map结构
allUsers[newUser.id] = newUser
//启动go程,将msg返回给客户端
go writeBackToClient(&newUser, conn)
//向message写入通知消息,广播当前用户上线通知
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!\n", newUser.id, newUser.name)
message <- loginInfo
fmt.Println("message接收到的消息是:", loginInfo)
//创建一个用于重置连接计数器的管道,用于告知watch函数,当前用户正在输入
var resTimer = make(chan bool)
//定义退出信号,用于监听client退出
var isQuit = make(chan bool)
//启动go程负责监听退出信号
go watch(&newUser, conn, isQuit, resTimer)
for {
//具体业务逻辑
buf := make([]byte, 1024)
//读取客户端发送过来的请求数据
cnt, err := conn.Read(buf)
if cnt == 0 {
fmt.Println("客户端ctrl+c,准备退出...")
//清理,map删除用户、conn close
//服务器还可以主动退出
//在这里不进行真正的退出,而是发送一个退出信号,统一做退出处理,可以使用新的管道做信号传递
isQuit <- true
}
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
//cnt-1:去掉最后的回车
fmt.Println("客户端接收发送过来的数据:", string(buf[:cnt-1]), "cnt:", cnt)
//----------业务逻辑代码 开始----------
//1. 查询当前所有用户:who命令
// a. 判断接收的数据是不是who ===> 长度&&字符串
userInput := buf[:cnt-1] //这是用户输入的数据,去掉回车
if len(userInput) == 4 && string(userInput) == "\\who" {
// b. 遍历allUsers这个map:(key := user.id value := user本身),将id和name拼接成一个字符串,返回给客户端
fmt.Println("用户查询所有用户:")
//创建切面用于包含所有的用户信息,以便返回给客户查询(切面服务直接返回的)
var userInfos []string
for _, user := range allUsers {
userInfo := fmt.Sprintf("userId: %s, userName: %s\n", user.id, user.name)
userInfos = append(userInfos, userInfo)
}
//最终写入管道中,一定是一个字符串
r := strings.Join(userInfos, "\n")
//将数据返回给查询的客户端
newUser.msg <- r
} else if len(userInput) > 9 && string(userInput[:7]) == "\\rename" {
// ● 规则:rename | Duke
// ○ 读取数据判断长度大于8,判断字符是rename
// ○ 使用 | 分割,获取 | 后面的作为名字
fmt.Println("用户修改名称")
// ○ 更新用户名字newUser.name = Duke
// array := strings.Split(string(userInput), "|")
// name := array[1]
newUser.name = strings.Split(string(userInput), "|")[1]
//更新map中的user
allUsers[newUser.id] = newUser
fmt.Println("用户更新名称后:", allUsers)
// ○ 通知客户端更新成功
newUser.msg <- "rename successfuly" + "\n"
} else {
//如果不是命令,那么只需要写入广播通道即可,由其他go程进行常规转发
message <- string(userInput)
}
resTimer <- true
//----------业务逻辑代码 结束----------
}
}
//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User, conn net.Conn) {
//TODO
fmt.Printf("User:%s的go程正在监听自己的msg管道\n", user.name)
for data := range user.msg {
fmt.Printf("user:%s 写回给客户端数据为:\n%s \n", user.name, data)
_, _ = conn.Write([]byte(data))
}
}
//向所有用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast程序退出!")
for {
fmt.Println("broadcast监听message中...")
//1. 从message读取数据
info := <-message
//将数据写入每一个用户的msg管道
for _, user := range allUsers {
//如果msg是非缓冲的,将会阻塞
user.msg <- info + "\n" //返回客户端后光标换行
}
}
}
//启动另一个go程,负责监听退出信号,触发后,进行清理工作:delete map,conn close都在这里处理
func watch(user *User, conn net.Conn, isQuit, resTimer <-chan bool) {
fmt.Println("启动监听退出信号go程...")
defer fmt.Println("watch退出了...")
for {
select {
case <-isQuit:
logoutInfo := fmt.Sprintf("%s exit already!(主动) \n", user.name)
fmt.Println("删除当前用户(主动):", user.name)
delete(allUsers, user.id)
message <- logoutInfo
conn.Close()
return
case <-time.After(5 * time.Second): //超过指定时间退出
logoutInfo := fmt.Sprintf("%s timeout exit already!(超时) \n", user.name)
fmt.Println("删除当前用户(超时):", user.name)
delete(allUsers, user.id)
message <- logoutInfo
conn.Close()
return
case <-resTimer:
fmt.Printf("连接%s重置计数器...\n", user.name)
}
}
}