这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记。
基础语法有几点需要注意:
有值类型,有指针,指针只能作为引用的替代品,无法指针直接运算。
go语言有值类型,可以直接像下面这样定义变量:
type Student struct {
name string
sid string
}
func main(){
var student = Student{name: "John", sid: "1001"} //student为值类型
var student = &Student{name: "John", sid: "1001"} //student为指针类型(注意由于go有垃圾回收机制,所以这里会自动为我们开辟堆内存
student := new(Student) //也可通过内置的new()函数直接开辟堆内存,而不立马初始化,得到一个指针
}
go语言的切片
同样切片类型也有上述两种获得内存的定义方式,也可通过内置的make函数对内部的cap和len进行初始的控制。
nums := make([]int,2,10)//得到一个底层数组长度为2,cap为10的初始切片
nums1 := nums2[0:3] //第二种切片方式
通过在字段后面跟着的字符串进行序列化的定义,后面跟着的称为域标签。
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"`
Sid string `json:"sid"`
}
func main() {
s := Student{Name: "jonh" ,Sid: "10323"}
//序列化
p ,err := json.Marshal(s)
if err!=nil {
panic(err)
}
fmt.Println(string(p))
//反序列化
err = json.Unmarshal(p,&s)
if err!=nil {
panic(err)
}
fmt.Println(s)
}
官方对域标签有以下说明:
// Field appears in JSON as key "myName".
Field int `json:"myName"`
// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`
// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`
// Field is ignored by this package.
Field int `json:"-"`
// Field appears in JSON as key "-".
Field int `json:"-,"`
时间的获取
获取当前时间:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now() //获取当前时间
fmt.Printf("current time:%v\n", now)
year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
获取时间戳
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now() //获取当前时间
timestamp1 := now.Unix() //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
fmt.Printf("现在的时间戳:%v\n", timestamp1)
fmt.Printf("现在的纳秒时间戳:%v\n", timestamp2)
}
时间戳与时间的转换
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now() //获取当前时间
timestamp := now.Unix() //时间戳
timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
fmt.Println(timeObj)
year := timeObj.Year() //年
month := timeObj.Month() //月
day := timeObj.Day() //日
hour := timeObj.Hour() //小时
minute := timeObj.Minute() //分钟
second := timeObj.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
获取星期几
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Println(t.Weekday().String())
}
时间的操作
(1)Add(during)函数实现某个时间 + 时间间隔
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
later := now.Add(time.Hour) // 当前时间加1小时后的时间
fmt.Println(later)
}
(2)Sub(Time)获取时间差值
返回一个时间段 t - u 的值。如果结果超出了 Duration 可以表示的最大值或最小值,将返回最大值或最小值,要获取时间点 t - d(d 为 Duration),可以使用 t.Add(-d)。
(3)Equal(Time)判断时间是否相同
(4)Before 和 After某个时间是否在他之前或之后
定时任务
使用 time.Tick(时间间隔) 可以设置定时器,定时器的本质上是一个通道(channel)
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i) //每秒都会执行的任务
}
}
解析字符串格式的时间
Parse 函数可以解析一个格式化的时间字符串并返回它代表的时间。
func Parse(layout, value string) (Time, error)
与 Parse 函数类似的还有 ParseInLocation 函数。
func ParseInLocation(layout, value string, loc *Location) (Time, error)
ParseInLocation 与 Parse 函数类似,但有两个重要的不同之处:
示例代码如下:
package main
import (
"fmt"
"time"
)
func main() {
var layout string = "2006-01-02 15:04:05"
var timeStr string = "2019-12-12 15:22:12"
timeObj1, _ := time.Parse(layout, timeStr)
fmt.Println(timeObj1)
timeObj2, _ := time.ParseInLocation(layout, timeStr, time.Local)
fmt.Println(timeObj2)
}
字符串与数字互转的想关库函数全在一个包内:strconv包
一图胜千言:
os包里面封装了很多和操作系统相关的内容。
如下:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
fmt.Println(os.Args) //打印命令行参数
fmt.Println(os.Getenv("PATH")) //打印环境变量
fmt.Println(os.Setenv("AA","BB")) //设置环境变量,key,val形式设置
buf,err := exec.Command("grep").CombinedOutput() //执行cmd命令
if err != nil {
panic(err)
}
fmt.Println(string(buf))
}
其他pass跳过
想要实现在线词典,首先就得用到别人的翻译引擎
以彩云词典为例:
从网页调试工具里面查看随时收发的网络数据包,挨个查看它们的response,如果里面的json数据出现翻译结果,那么说明这个包就是返回的翻译结果!
那么我们只需要让go语言来做同样的两件事:
只要做好了这两件事,那么就很快得到了一个单词的翻译了。
在此之前我们需要清楚有两个神器般存在的网站:
那么我们先肯定是要得到请求的代码,然后稍作更改,解析body后得出想要的结果。
如下图进到刚才我们捕捉到的目标包,然后复制cURL,到网站进行解析得到最终代码。
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Connection", "keep-alive")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
req.Header.Set("app-name", "xy")
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("os-type", "web")
req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
}
观察代码的改变我们只需对source部分的内容进行更改,即可得到对应的翻译结果。
得到翻译结果,body内容后,我们需要把body内容解析为本地的sturct才能正常使用(当然你头铁的话可以直接找对应的字符串即可,也不需要反序列化。
type AutoGenerated struct {
Rc int `json:"rc"`
Wiki struct {
KnownInLaguages int `json:"known_in_laguages"`
Description struct {
Source string `json:"source"`
Target interface{} `json:"target"`
} `json:"description"`
ID string `json:"id"`
Item struct {
Source string `json:"source"`
Target string `json:"target"`
} `json:"item"`
ImageURL string `json:"image_url"`
IsSubject string `json:"is_subject"`
Sitelink string `json:"sitelink"`
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []interface{} `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
通过前面生成的代码已经能够实现请求和接收响应,且可以直接把响应内容反序列化为结构体,那么接下来,只需要把想要的部分遍历打印即可。
最终代码如下:
package src
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func QueryCaiyun(word string) {
client := &http.Client{}
request := DictRequestCaiyun{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Connection", "keep-alive")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
req.Header.Set("app-name", "xy")
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("os-type", "web")
req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != 200 { //防止返回错误
log.Fatal("bad Status code:", resp.StatusCode, "body", string(bodyText))
}
var dictResponse DictResponseCaiyun
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Println("translate from Caiyun\n", "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
}
后面我依次通过这个方式还弄了其他的翻译引擎,但是有很多翻译引擎的请求头都是动态实时的,或者加了密。我做的第二个有道的翻译引擎使用就是动态实时的,然后去查阅了破解方法,发现是通过阅读原本的js源码进行反推,得出请求头里面动态变化的内容。
最终写了Deepl和有道两个搜索引擎。
源码实现链接:https://github.com/ACking-you/TraningCamp/tree/master/lesson1/homework/simple_dict/src
SOCKS5是代理协议,在使用TCP/IP协议通信的前端机器和服务器之间发挥中介作用,使内部网络的前端机器能够访问互联网的服务器,使通信更加安全。SOCKS5服务器将前端发送的请求转发给真正的目标服务器,模拟前端行为。此处,前端与SOCKS5之间也是通过TCP/IP协议进行通信的,前端向SOCKS5服务器发送请求发送给SOCKS5服务器,然后SOCKS5服务器将请求发送给真正的服务器。SOCKS5服务器在将通讯请求发送给真正服务器的过程中,对于请求数据包本身不加任何改变(明文传输)。SOCKS5服务器在收到真实服务器响应后,也原样转发到前端。
它的用途是, 比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。 socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多软件,最终暴露的也是一个 socks5 协议的端口。
正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立 TCP 连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。如果设置代理服务器之后,流程会变得复杂一些。 首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段。
package main
import (
"bufio"
"fmt"
"log"
"net"
)
func main() {
server, err := net.Listen("tcp", "0.0.0.0:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
fmt.Printf("连接成功! clent:%v \n", client.RemoteAddr())
go process(client)
}
}
func process(conn net.Conn) {
defer func() {
conn.Close()
fmt.Printf("连接断开! clent:%v \n", conn.RemoteAddr())
}()
//用缓冲流进行一次包装,减少底层IO次数,让读取效率更高效
reader := bufio.NewReader(conn)
for {
b, err := reader.ReadByte()
if err != nil {
break
}
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
客户端验证:
没必要再写一个客户端,这时完全可以netcat工具进行tcp连接测试。
如下:
实现SOCKS5之前我们需要清楚SOCKS5的握手阶段的请求和返回是怎么样的,如下面的图表所示:
VER | NMETHODS | METHODS |
---|---|---|
1byte | 1byte | 1 to 255 byte |
协议版本信息socks5为0x05 | 支持认证的方法数量值为0x00则表示无需认证 | NMETHODS的值为多少METHODS就有多少个字节 |
package main
//auth 阶段
import (
"bufio"
"fmt"
"io"
"log"
"net"
)
const(
socks5Ver = 0x05
cmdBind = 0x01
atypIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
func main() {
server, err := net.Listen("tcp", "0.0.0.0:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
fmt.Printf("连接成功! clent:%v \n", client.RemoteAddr())
go process(client)
}
}
func process(conn net.Conn) {
defer func() {
conn.Close()
fmt.Printf("连接断开! clent:%v \n", conn.RemoteAddr())
}()
reader := bufio.NewReader(conn)
err := auth(reader,conn)
if err!=nil{
log.Printf("client %v auth failed:%v",conn.RemoteAddr(),err)
}
log.Println("auth success")
}
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
//协议版本
ver,err := reader.ReadByte()
if err != nil{
return fmt.Errorf("read ver failed:%w",err)
}
if ver != socks5Ver{
return fmt.Errorf("not supported ver:%v",ver)
}
//支持的方法数量
methodSize,err := reader.ReadByte()
if err!=nil{
return fmt.Errorf("read methodSize failed:%w",err)
}
//方法值
method := make([]byte,methodSize)
_,err = io.ReadFull(reader,method)
if err!=nil{
return fmt.Errorf("read method failed %w",err)
}
log.Println("ver",ver,"method",method)
//返回的内容表示SOCKS5通信,且无需认证
_,err = conn.Write([]byte{socks5Ver,0x00})
if err !=nil{
return fmt.Errorf("write failed:%w",err)
}
return nil
}
同样来看看此时的消息协议:
客户端请求:
VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1byte | 1byte | 1byte | 1byte | Variable | 2byte |
协议版本0x05为SOCKS5 | 代表请求类型0x01表示CONNECT请求 | 保留字段(不理会) | 目标地址类型(IPV4/IPV6/域名) | 地址值,根据不同地址类型,长度不同 | 需要访问的服务器的端口号 |
服务端响应:
VER | REP | RSV | ATYP | BIND.ADDR | BIND.PORT |
---|---|---|---|---|---|
1byte | 1byte | 1byte | 1byte | Variable | 2byte |
协议版本0x05为SOCKS5 | 代表响应。成功就返回0 | 保留字段(不理会) | 地址类型(IPV4/IPV6/域名) | 地址值(这里暂时不需要 | 端口号(这里暂时不需要 |
这一过程的代码:
对当前的实现进行测试:
进行如下curl命令:
curl --socks5 localhost:1080 -v http://www.qq.com
此时请求会失败,但我们已经能看到正常打印出来的ip和端口号
最后的转发过程,由于不需要对流量进行任何的处理,所以没有上层协议,直接再Write操作完后把流量进行转发即可。
对于两个连接流量的转发,标准库里有有一些好用的函数库。
通过net.Dial建立tcp连接。
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
标准库的 io.copy 可以实现一个单向数据转发,双向转发的话,需要启动两个 goroutinue。
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向copy出错的时候,再返回 connect 函数。
这个context目前还弄不明白
这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context。 在最后等待 ctx.Done() , 只要 cancel 被调用, ctx.Done就会立刻返回。 然后在上面的两个 goroutinue 里面 调用一次 cancel 即可。
同样是以下请求命令此时终于返回正常内容了!
curl --socks5 localhost:1080 -v http://www.qq.com
现在SOCKS5代理服务器程序已经写好,可以使用SwitchyOmega插件对该代理服务器进行正式的使用(可以用连接了学校内网的电脑,作为SOCKS5代理服务器对学校内网的内容进行访问( ̄▽ ̄)"