猜数字
使用bufio流式处理来自stdin的输入
reader := bufio.NewReader(os.stdin)
str, err := reader.ReadString('\n')
用strconv包的Atoi(string) int方法将字符串转为数字,在此之前用strings的TrimSuffix(string, suffix) string方法把字符串末尾的换行"\n"删除(CRLF是"\r\n")
num, err := strconv.Atoi(strings.TrimSuffix(str, "\n"))
就完成了读入数字的操作剩下的就是写点分支不做赘述
命令行词典
主要的收获是几个好用的网站,以前一直是手动造各种请求头,手动处理返回的json,麻烦又容易出错,一直没想去找点工具。 这里主要是把生成的代码再过一遍,依赖工具的同时不能忘了怎么手写。
新建一个http.Client
client := &http.Client{}
初始化请求body并且进行序列化
request := DictRequest{
TransType: "en2zh",
Source: word,
}
buf, err := json.Marshal(request)
用buf创建一个流,并且新建请求
data := bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
设置请求头
req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("accept", "application/json, text/plain, */*")
// ... 后面省略一万个请求头
用client.Do(req)发起请求
resp, err := client.Do(req)
别忘了加上defer
defer resp.Body.Close()
然后就可以获得返回的body文本
bodyText, err := io.ReadAll(resp.Body)
之前的包括这里检查err的代码都省略了,在此处还需要注意status code是否为200,因为有可能不为200返回的body是错误的数据
反序列化字典响应
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
之后简单打印一下就ok了
可以命令行传入参数,通过os.Args来获取并且依次翻译,运行如下
socks5 代理服务器
首先要监听一个端口,不断用goroutine处理来自客户端的连接
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
conn, err := server.Accept() // 等待一个客户端的连接
if err != nil {
log.Fatal("Accept failed:", err) // 打印错误信息
continue
}
go handle(conn) // 处理客户端连接
}
对于handle方法,主要的逻辑是:先读入请求的头进行验证,然后再获取连接的具体内容建立双向的读写,简要的代码如下
func handle(conn net.Conn) {
defer conn.Close() // 延迟关闭conn
reader := bufio.NewReader(conn) // 读入流
err := auth(reader, conn) // 验证
... // err
err = connect(reader, conn) // 连接
... // err
}
auth是读入ver, methodSize, method三个部分,分别对应版本、方法大小、方法,其中前两个都是一字节,最后一个method长度是methodSize指定的。用readByte() (byte, error)方法读入字节后检查err然后还要核验版本号的合法性。读入方法的时候先make([]byte, methodSize)创建一个长度为methodSize的缓冲区,然后io.ReadFull一次性读完。
/* 验证请求头 */
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// 版本号
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read header failed: %v", err)
}
if ver != socks5Ver {...}
// 方法大小
size, err := reader.ReadByte()
if err != nil {...}
// 方法
method := make([]byte, size)
_, err = io.ReadFull(reader, method)
if err != nil {...}
// 写回response
_, err = conn.Write([]byte{ver, 0x00})
if err != nil {...}
return nil
}
连接部分在前面的读取部分几乎是一样的,要读ver,cmd,rsv,atype四个字节的头(对应版本、指令、保留字、地址类型),可以创建一个四字节的缓冲区buf一次性读入,核验完版本和指令之后,根据不同的atype,对应的获取实际的地址,其中0x01是ipv4,后跟四个字节,0x03是不定长的域名,后跟不定长的字符串,同样的第一个字节是数据长度,读入后再读该长度的就是实际的地址。最后读入两字节的port,可以用切片buf[:2]复用buf,然后通过binary.BigEndian.Uint16(buf[:2])获取到port。
在进行了上述操作之后,我们会得到地址addr,端口port。之后用net.Dial建立和实际服务器的连接。成功后给客户端返回一个响应。我们使用context库来等待双向连接结束,在读写错误时直接退出。
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
...
// 建立连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial %v:%v failed: %v", addr, port, err)
}
defer dest.Close()
// 写回response
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {...}
// 创建一个context,当cancel被调用时,ctx.Done才会触发
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader) // 浏览器(用户)数据发送给到实际的服务器
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest) // 服务器返回的内容写回给浏览器(用户)
cancel()
}()
// 等待连接结束
<-ctx.Done()
return nil
}
最后实际的运行,设置浏览器代理后访问pkg.go.dev/