看懂例子把它理解好,知道实现的原理是什么,还可以自己实现出来。
忘了就看day2的讲解,里面的函数都懂,这个并不难。
首先要知道tcp聊天室的实现理论:客户端采用简单的输入输出方式进行消息的发送和接收,服务端通过广播的方式将信息发送给所有的客户端。
服务端例子
package main
import (
"fmt"
"net"
)
var (
clients = make(map[net.Addr]net.Conn)
addCh = make(chan net.Conn)
delCh = make(chan net.Addr)
messageCh = make(chan []byte)
listenAddr = "localhost:8080"
)
func main() {
fmt.Println("Server started on", listenAddr)
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Println(err)
return
}
defer listener.Close()
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
continue
}
addCh <- conn
go handleConn(conn)
}
}
func broadcaster() {
for {
select {
case conn := <-addCh:
clients[conn.RemoteAddr()] = conn
fmt.Println("New client:", conn.RemoteAddr())
case addr := <-delCh:
delete(clients, addr)
fmt.Println("Client disconnected:", addr)
case msg := <-messageCh:
for _, conn := range clients {
_, err := conn.Write(msg)
if err != nil {
fmt.Println(err)
}
}
}
}
}
func handleConn(conn net.Conn) {
defer func() {
delCh <- conn.RemoteAddr()
conn.Close()
}()
for {
msg := make([]byte, 4096)
n, err := conn.Read(msg)
if err != nil {
return
}
messageCh <- msg[:n]
}
}
常量解读:
clients:这里客户登记使用了键值对的形式,创建了一个map来登记连接,key是客户端的地址,value是与该地址建立的连接。
addCh :net.Conn类型的通道,这个是用于实现客户端连接建立。
delCh:net.Addr类型的通道,用于实现客户端连接的删除。
messageCH :字节切片类型的通道,放要传输的数据。
listenAddr:监听的地址和端口号。
可能有这样的一个疑问:这里为什么要用通道,用通道有个好处,在并发的环境下不会产生冲突,保证了并发安全。因为是模拟聊天室,那就要考虑一时间有好多个用户接入的情况,我们在操作map的时候就必须保证并发安全,这里保证map的并发安全就使用了通道来进行实现。
main主函数逻辑:
服务器端先调用了net.Listener,先创建一个监听器,然后启动广播协程:这个协程主要是用于监听每一个通道的,等下再说这个函数的细节。然后服务器有个无限循环,因为启动监听之后,就要开始监听客户端发来的请求了,这里一般都是无限循环然后调用listener.Accept()来接收客户端的请求,一旦请求接收成功就建立了Conn,可以进行数据的收发。既然有连接建立那就要进行连接的登记,也就是for里面有个处理:addCh <- conn,这就是连接的登记,把连接送入addCh通道里面。然后启动一个处理函数协程handleConn对连接进行处理。
func broadcaster()函数逻辑
首先要明白select的性质,select是go并发编程里面的内容,与通道相关,每个case都是一个通道操作,当有case的通道操作能够执行,那就会执行通道语句,当有多个通道case有效,会随机选择一个进行执行,这里是for无限循环,那就会处理所有的通道操作。当没有通道操作的时候这个select语句就会阻塞。
所以说这个函数就是来处理所有的通道操作的。来看里面每一个通道的处理
case conn := <-addCh:
clients[conn.RemoteAddr()] = conn
这个是进行登记处理,net.Conn类型里面的这个函数是登记远端的IP地址,对于服务器来说那就是用户的IP地址。conn就是这个连接,注意这里我前面说过了是采用map来登记。
case addr := <-delCh:
delete(clients, addr)
这个就是连接断开,然后进行登记的删除,这个delete也是调用了map里面的一个api,删除指定的key也就是addr。
case msg := <-messageCh:
for _, conn := range clients {
_, err := conn.Write(msg)
if err != nil {
fmt.Println(err)
}
}
这个通道处理是进行消息广播的,这里for range clients,就是拿到每一个连接然后把msg发送到这个连接去。可以看到拿到连接后调用了conn.Write(msg)把消息写入每个用户的连接中,达到数据广播的效果。
func handleConn(conn net.Conn)的逻辑,这个函数是用来进行拿到连接后的处理的。因为同一时间要进行操作用户很多,所以要启动协程来进行处理。
func handleConn(conn net.Conn) {
defer func() {
delCh <- conn.RemoteAddr()
conn.Close()
}()
for {
msg := make([]byte, 4096)
n, err := conn.Read(msg)
if err != nil {
return
}
messageCh <- msg[:n]
}
}
defer这里是进行了模拟用户操作完后的连接关闭,所以这里就要给delCH通道发个信息,进行登记的删除,然后关闭这个conn。
for循环是进行了无限循环从连接里面读取数据,因为,msg是广播的。所以每个用户都要随时接收连接里面的数据。
这里接收完数据之后也会做一个messageCh <- msg[:n],也是再次进行消息的发送,这个消息会广播出去。
总结:把握好tcp聊天室的理论原理就是用户输入数据后,服务器负责将消息广播给每个用户。这样就能很好的理解每个操作是为了什么。
实现原理:main函数负责启动开启服务器监听,然后启动通道处理,启动用户请求处理。通道处理复杂所有通道的功能,启动用户请求处理是处理每个用户。
golang UDP聊天室
package main
import (
"fmt"
"net"
"os"
"strings"
)
const (
serverAddr = "localhost:8080"
)
func main() {
// 解析UDP地址
addr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
fmt.Println(err)
return
}
// 建立UDP连接
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
fmt.Println("Server started on", serverAddr)
// 用于保存客户端的地址
clients := make(map[string]*net.UDPAddr)
for {
// 接收请求
buf := make([]byte, 1024)
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println(err)
continue
}
// 解析客户端请求
msg := strings.TrimSpace(string(buf[:n]))
if msg == "" {
continue
}
fmt.Printf("Received %d bytes from %s: %s\n", n, clientAddr, msg)
// 如果是新客户端,添加到客户端列表中
if _, ok := clients[clientAddr.String()]; !ok {
fmt.Println("New client:", clientAddr)
clients[clientAddr.String()] = clientAddr
}
// 广播消息给其他客户端
for _, addr := range clients {
if addr.String() == clientAddr.String() {
continue // 不发送给发送者
}
_, err := conn.WriteToUDP([]byte(msg), addr)
if err != nil {
fmt.Println(err)
}
fmt.Printf("Sent %d bytes to %s: %s\n", len(msg), addr, msg)
}
}
}
解读:
1.UDP和tcp有点区别,这个解析地址的步骤要单独写出来。解析本地地址,端口号8080,调用ResolveUDPAddr()之后会得到解析后的UDP地址UDPAddr,然后进行调用ListenUDP()监听客户端的UDP请求,返回操作数据的连接。
2.这里还是用map来进行保存客户端的地址。
我学这里的时候我会去对比tcp建立聊天室的实现,我发现udp的监听这里不是无限循环监听,这里其实是我理解错了,因为udp是无连接的,所以这里我是这么来方便理解的,相当于这里调了监听函数,就是服务器UDP开了个口子,然后客户端根本不用建立连接,只管往这个UDPConn里面发数据就完事了。
3.所以说真正的无限循环是在直接对数据进行处理这里。
下面这个无限循环for,直接调用这个conn的读数据,从里面读udp客户端的请求,n, clientAddr, err := conn.ReadFromUDP(buf)调用这个函数,把数据读到buf中,并返回远端的IP地址。
把数据清理后赋给msg并输出。
4.下面访问键值对看这个地址原来在不在,不在就是新连接进行登记。
法消息的时候遍历这个map,这样调用func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error),可以把数据广播给登记的IP地址。这就完成了广播功能。
客户端:
package main
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
const (
serverAddr = "localhost:8080"
)
func main() {
// 解析服务器地址
raddr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
fmt.Println(err)
return
}
// 建立与服务器的连接
conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 从标准输入读取数据
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter message: ")
message, _ := reader.ReadString('\n')
// 向服务器发送数据
_, err = conn.Write([]byte(message))
if err != nil {
fmt.Println(err)
return
}
// 设置读取超时
err = conn.SetReadDeadline(time.Now().Add(15 * time.Second))
if err != nil {
fmt.Println(err)
return
}
// 从服务器接收数据
buf := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Received: %s\n", string(buf[:n]))
}
分析:
客户端也是先解析服务器ip地址
然后调用,net.DialUDP(“udp”, nil, raddr),这样就拿到了conn,相当于建立了连接。(这里注意这并不是真正意义上的连接,只是我这样形容)。
拿到这个conn用户就可以往服务器发数据了,我要发的数据全装在这个reader里面,然后调用reader的Readstring方法把内容给message。
然后调用conn的写把mes传进去。这样就完成了用户发送数据。
下面还设置了一个读取超时。15秒服务器没读就会发生错误。
从服务器接收数据用到了ReadFromUDP,读到的数据都存在了buf里面。
基本逻辑就是这样。
专业说法:什么是URL(统一资源定位符),是一种用于定位互联网上资源的地址,它是互联网上标准资源的地址,可以用来访问网页、图片、视频或其他类型的数据。
通俗理解:你在网上点击的每个链接,都是URL,因为你点链接的过程实际上就是请求资源的过程。这个URL就是定义好的访问资源的通用方法。
一个URL通常包含以下几个部分:
1.协议(scheme):定义了访问资源的方法,比如http/https/ftp等
2.域名或IP地址(host):制定了托管资源的服务器位置
3.端口号(可选)(port):用于访问服务器上的特定服务,比如HTTP协议端口号是80,HTTPS是443。
4.路径(path):指定服务器上资源的具体位置
5.查询字符串(可选)(query):以键值对的形式提供额外参数,通常用于提交表单数据或指定资源的某种特定视图。
6.片段标识符(fragment):用于指向网页内的特定部分。
举个例子:这个例子会分析就相当于学会啥是URL了。
https://www.example.com:443/path/to/myfile.html?key1=value1&key2=value2#Section2
分析:
协议:https
域名:www.example.com
端口号443
路径/path/to/myfile.html
查询字符串:key1=value1&key2=value2
注意这里是键值对形式
片段标识符:Section2
//:是协议分隔符,用于分割协议
?是查询字符串的开始,后面跟着一系列键值对,每对键值对用&隔开。
#是片段标识符的开始,它通常用于指向网页中的特定部分。浏览器会滚动到页面中ID为该标识符的元素处。注意片段标识符不会被发送到服务器,它只客户端(i浏览器上使用)。
go的标准库net/url包提供了URL编程的实现,该包提供了一些功能,使得我们可以解析、构建和操作URL字符串,注意是字符串,我们平时点的就是字符串。
主要用到了net/url包中的Parse函数,该函数可以将URL字符串解析为URL结构体,该结构体包含了URL的各个组成部分.
package main
import (
"fmt"
"net/url"
)
func main() {
// 解析URL
u, err := url.Parse("https://www.example.com/search?q=golang#top")
if err != nil {
fmt.Println(err)
return
}
// 输出URL的各个部分
fmt.Println("Scheme:", u.Scheme)
fmt.Println("Host:", u.Host)
fmt.Println("Path:", u.Path)
fmt.Println("Query:", u.Query())
fmt.Println("Fragment:", u.Fragment)
}
输出的内容:
Scheme: https
Host: www.example.com
Path: /search
Query: map[q:[golang]]
Fragment: top
u就是结构体,下面这些都是它里面的属性和方法
总结url.Parse(url string) ,返回值是URL结构体和一个可能发送的错误。
然后就可以操作这个结构体。
就是用这个URL结构体转回字符串,相当于上面逆过来。
package main
import (
"fmt"
"net/url"
)
func main() {
// 构建URL
u := &url.URL{
Scheme: "https",
Host: "www.example.com",
Path: "/search",
}
q := u.Query()
q.Set("q", "golang")
u.RawQuery = q.Encode()
// 输出URL字符串
fmt.Println(u.String())
}
解读
u.Query() 用于返回一个url.Values类型,代表URL的查询字符串,这个查询字符串就是上面介绍部分的那个。url.Values是什么类型:map[string][]string,key是字符串,value是字符串切片,这意味着每个键可以对于多个值,这是因为在URL的查询字符串中,同一个键可以有多个值,例如:
?key=value1&key=value2
q.Set(“q”, “golang”) 这个事Values类型的一个方法,用于向查询字符串中添加一个键值对"q=golang"
u.RawQuery = q.Encode()
使用Encode方法,将修改后的查询参数编码为字符串,并将其赋值给u的RawQuery字段。这样URL的查询部分就被设置为"q=golang"
代码运行输出结果:
https://www.example.com/search?q=golang
要解析查询参数可以使用net/url包中的Values类型。将查询字符串作为传输传递给Values函数,然后通过Get方法来获取特定参数的值。
package main
import (
"fmt"
"net/url"
)
func main() {
// 解析查询参数
values, err := url.ParseQuery("q=golang&sort=recent&limit=10")
if err != nil {
fmt.Println(err)
return
}
// 获取特定参数的值
q := values.Get("q")
sort := values.Get("sort")
limit := values.Get("limit")
fmt.Println("q:", q)
fmt.Println("sort:", sort)
fmt.Println("limit:", limit)
}
解读:
values, err := url.ParseQuery(“q=golang&sort=recent&limit=10”) 用于解读一个URL编码格式的查询字符串。这个字符串包含三个键值对,解析的结果是values类型的,存储在遍历values中。
values.Get(“q”) 用于获取特定参数的值,里面的参数是key,返回值是value
代码运行输出
q: golang
sort: recent
limit: 10
要对URL字符串进行编码和解码就要用net/url包下面的QueryEscape和QueryUnescape函数,分别实现对URL字符串进行编码和解码。
package main
import (
"fmt"
"net/url"
)
func main() {
// 编码字符串
encoded := url.QueryEscape("https://www.example.com/search?q=golang&sort=recent")
fmt.Println("Encoded:", encoded)
// 解码字符串
decoded, err := url.QueryUnescape(encoded)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Decoded:", decoded)
}
解读:
encoded := url.QueryEscape(“https://www.example.com/search?q=golang&sort=recent”)
用于编码字符串,对给定的字符串进行百分比编码,这个对于编码URL的查询字符串部分很有用,因为对处理URL特殊字符(& = ?)时,会把这些特殊字符转换为百分比形式。
这里的输出结果:
Encoded: https%3A//www.example.com/search%3Fq%3Dgolang%26sort%3Drecent
decoded, err := url.QueryUnescape(encoded)
解码就是进行还原
输出:
Decoded: https://www.example.com/search?q=golang&sort=recent
一个疑问:为什么要换成百分比?
将URL中的特定字符串替换为百分比,这种被定义为URL编码。
这种编码的好处:
1.保留字符:URL中某些字符有特殊含义,百分比编码可以在不改变原有意义的情况下安全地包含这些字符。
2.非ASCII字符,对于某些在ASCII中有特殊含义的字符必须使用百分比编码来标识
3.安全性:百分比编码有助于消除URL中可能引起安全问题的字符。
4.一致性和标准性:通过百分比编码可以确保URL的一致性和标准化,使得不同的网络设备和软件能够正确解析URL。
这种编程是通过net/http包实现的。
这个标注库提供了完善的HTTP客户端和服务器实现。所以可以在go语言实现编写HTTP相关的应用。
直接来看demo,这些都是这个包的应用
HTTP客户端
一个简单的使用net/http包发送 HTTP GET请求的例子
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("http://www.example.com/")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
解读:
resp, err := http.Get(“http://www.example.com/”) ,调用http下的这个函数可以向这个指定的网站发送一个GET请求,这个函数非常常用于从指定的URL获取数据。
参数就是一个url,就是要请求的url,字符串类型的。
resp是*http.Response类型的指针,它标识服务器的响应。err是请求过程中可能发生的错误。
当这个函数调用成功,它会向这个URL发送一个GET请求,如果成功,resp将是服务器的响应。这个响应前面学过理论基础,就是服务器那边发来的信息。包含状态码、响应头以及响应体等信息。如果请求失败err会描述发生的错误。
*http.Response类型解读:这是一个结构体,下面注意介绍里面的字段。
Status:
类型:string
描述:响应的状态行,例如 “200 OK”。这个字段包含了状态码和状态描述。
StatusCode:
类型:int
描述:数字形式的 HTTP 状态码,例如 200、404 等。
Header:
类型:http.Header(实质上是 map[string][]string)
描述:响应头。这是一个映射表,包含了所有的响应头字段和值。每个头字段可以有一个或多个值。
Body:
类型:io.ReadCloser
描述:响应的主体。这是一个 io.Reader 接口,用于读取响应的主体内容。它还实现了 io.Closer 接口,这意味着在读取完毕后,你需要调用 Body.Close() 来关闭它,释放资源。
ContentLength:
类型:int64
描述:响应主体的长度。如果长度未知,该值为 -1。
TransferEncoding:
类型:[]string
描述:传输编码列表,按照应用的顺序。
Close:
类型:bool
描述:指示是否应在读取完响应主体后关闭连接。
Request:
类型:*http.Request
描述:生成这个响应的 HTTP 请求。这对于跟踪请求-响应链很有用。
TLS:
类型:*tls.ConnectionState
描述:如果通过 HTTPS 访问,则包含有关 TLS 连接的信息。如果不是 HTTPS,则为 nil。
通过上述描述,这个resp完全就是服务器的响应的全部内容,我们可以按照需要选择字段进行操作。这里我们一般都去关注body这个字段:响应的主体,就是我们要的主要内容。
关于这个body的数据类型做一点说明:
type ReadCloser interface {
Reader
Closer
}
它是一个接口类型,它又嵌套了两个接口,一个接口用于从数据流中读数据,这个接口包含了个read方法,可以实现读取数据到指定字节切片中;另一个用于关闭数据流,它又一个close()方法,调用后关闭数据流。
这里有个注意就是处理完响应主题后,应该关闭resp.Body,这样做的原因和前面学的那些关闭几乎都一个原因,避免资源泄露。
body, err := ioutil.ReadAll(resp.Body)
用于读取响应体的数据,传的参数只要求实现了io.Reader接口就可以传,这样刚好符合。然后返回一个字节切片,这样就做到了把响应体里面的内容读出来。
综上:使用net/http包轻松的做到了向指定的URL发送http get请求,并且得到响应后输出响应的内容。
看例子,还是客户端
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
url := "http://www.example.com/login"
data := []byte(`{"username": "admin", "password": "password"}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
fmt.Println(err)
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
解读:
url := “http://www.example.com/login”
data := []byte({"username": "admin", "password": "password"}
)
url指向登录接口,下面这个json格式标识的用户名和密码。
req, err := http.NewRequest(“POST”, url, bytes.NewBuffer(data)) 这个函数用于创建一个新的HTTP请求。这个函数的作用是构造一个指定HTTP方法、URL和可选正文的HTTP请求。
结构:
func NewRequest(method, url string, body io.Reader) (*http.Request, error)
参数:方法;指定url;body是请求的正文,如果请求不需要正文,可以为nil。正文是在HTTP请求和响应中,传输的主要数据部分,就是头部的下面那部分,即实际要收发的数据内容。这个正文通常也是有格式的:文本(纯文本,json,xml),表单数据,二进制数据(文件)
在上面这个代码中,我请求的正文就是一个json格式的字节切片,这里由于是要io.Reader所以这里用了一个bytes.NewBuffer(data)创建了一个io.Reader对象,从而可以作为参数传入。
返回值:*http.Request标识构造的HTTP请求,可以说是请求的要素全部齐全了,包含了方法,URL,头部,正文等信息。
总结 这个函数起到了定制请求的效果,满足了对灵活性的需求,通过这个函数我可以构造几乎任何类型的HTTP请求。
req.Header.Set(“Content-Type”, “application/json”) 设置请求的请求正文的内容格式
client := &http.Client{}
resp, err := client.Do(req)
这里是创建一个实例来发送求情,使用client结构体的Do()方法,来发送这个创建的请求,并接收响应。这个resp就是接收的响应。
最后要关闭响应体。defer resp.Body.Close()
拿到的这个响应体,可以用
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
打印出来,因为resp.Body是io.Reader类型的。
下面是一个简单的服务器锂离子
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
解读:
*http.HandleFunc(“/”, func(w http.ResponseWriter, r http.Request) {
fmt.Fprintf(w, “Hello, World!”)
})
注册处理函数:这个函数用于注册一个处理函数,该函数用于处理对特定路径的HTTP请求。
这个代码中,这个函数被用来处理所有发往根路径"/"的请求。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
pattern,字符串类型,代表指定要处理的URL路径,handler一个函数,当接收到匹配这个路径的请求时,这个函数被调用。
*func(w http.ResponseWriter, r http.Request) 匿名函数,w http.ResponseWriter用于构建和发送HTTP响应,你可以通过w写入响应正文、设置响应的状态码、添加响应头等。
*r http.Request 代表接收到的HTTP请求。它包含了请求的各自信息,比如请求方法,URL,请求头,请求正文等。
综上:也就是服务器接收到了请求,就会输出响应w和hello world
http.ListenAndServe(“:8080”, nil) 用于启动HTTP服务器,第一个参数代表监听本地的8080端口,第二个参数是处理器,nil表示使用默认多路复用器,http.DefaultServeMux。
调用之后会起到HTTP服务器,并监听端口,等待并处理HTTP请求。
package main
import (
"net/http"
)
func main() {
http.Handle("/", http.FileServer(http.Dir("static")))
http.ListenAndServe(":8080", nil)
}
解读:
会把当前目录下的static文件夹作为根目录,提供静态文件服务,例如:客户端请求http://localhost:8080/index.html时,服务器返回的是static/index.html文件。
以上是http客户端和服务器的基本用法,这个包还有很多其他的功能,比如实现了WebSocket,HTTP长连接,Http代理等等。根据需要查询文档。
看day1
1.Gin:Gin是一个高性能、易用的HTTP框架,它提供了路由、中间件、静态文件服务、模板渲染等常用功能,并且支持自定义中间件和路由分组等高级特性。Gin的设计理念是尽量简单、快速地完成HTTP请求处理,并且提供高度可定制的API接口。
2.ECHO
3.Beego:Beego是一个完整的Web框架,它提供了MVC架构、ORM、Websocket、RESTful API等功能,并且支持国际化、日志、缓存等高级特性。Beego的设计理念是快速开发、易用可扩展,并且提供丰富的文档和社区支持。
4.Revel
还有很多框架,这里做了解,1是必须要学的,Beego看有没有多于的时间学。