1
头图

最近 Redis 作者 antirez 用C语言写了一个 https://github.com/antirez/smallchat —— 聊天服务器。看了下代码,逻辑清晰明了,非常适合新手学习。由于我是 Gopher,就想能否用 Go 实现一个聊天服务器。

说到聊天程序,就会想起大学时期,校内有个计算机工作室,加入要求是:自行实现一个聊天软件。
为了调程序,在图书馆泡了好久,把Windows API编程都翻烂了,也不知道github,只会baidu,觉得自己一定能写出一个让人眼前一亮的软件。最后磕磕巴巴写了个、一不小心就会触发bug的点对点聊天工具,总算过关。
不知道你的,第一次练手程序是什么?
好,今天带着当时对计算机的热情,来重温一遍聊天程序。

原程序

先研究下这个 smallchat ,看看运行起来是什么效果,拉取代码:

$ git clone https://github.com/antirez/smallchat

编译

这个是纯C代码,没有特别的依赖,直接编译就行。

$ cat Makefile # 查看 make 命令
$ make all # 编译所有

启动 server

$ ./smallchat-server # 启动 chat server

启动 client

# 启动 client 连接 server, 你可以启动多个 client
$ ./smallchat-client 127.0.0.1 7711  # 参数是 <服务IP> <服务端口> 端口号写在代码里了

运行效果如下:

show

Go 实现?

看了下C代码,inlcude 如下包:

  • socket
  • TCP
  • fd
  • stdio
  • malloc 内存管理
  • select 多路复用
  • termios 这个go也有对应实现,

这些功能 Go 都有,基本上不用引入第三方依赖就能实现。那就

开干

但我的 C语言 基础早已还给谭浩强老师了,写 PHPGolang 的我,在内存管理那块就看不懂了,所以,我准备借助 Copilot 来帮我把C语言翻译成Golang。

如今你还能拿出当年那份对计算机的热情么

Copilot

$ code .  # 启动宇宙最强编辑器--vscode

上号vscode

这里就借助比较容易访问的——Copilot,源代码也不长,直接全文复制到 copilot-chat 中,让它帮我们翻译成 golang 代码。

我把 chatlib.c smallchat-server.c 复制进去,很快就翻译好了。

chatlib.h, smallchat-server.h, smallchat-client.h 是头文件,只是声明一下函数定义,在Go中没有对应概念,不用转。

问答的效果图:
chat_copilot

前两个文件的转换都很顺利,甚至,我能用 smallchat-client.c 去访问 smallchat-server.go , 但是在翻译 smallchat-client.c 时,还是翻车了,出问题就在 termios 这个库的处理,我得到的回复是:

一下代码省略了部分
...
func main() {
    ...
    conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", os.Args[1], port))
    if err != nil {
        fmt.Println("Error connecting to server:", err)
        os.Exit(1)
    }
    defer conn.Close()

    if err := setRawMode(int(os.Stdin.Fd())); err != nil {
        fmt.Println("Error setting raw mode:", err)
        os.Exit(1)
    }

    go func() {
        scanner := bufio.NewScanner(os.Stdin)
        for scanner.Scan() {
            text := scanner.Text()
            if _, err := conn.Write([]byte(text + "\n")); err != nil {
                fmt.Println("Error writing to server:", err)
                os.Exit(1)
            }
        }
    }()

    scanner := bufio.NewScanner(conn)
    for scanner.Scan() {
        text := scanner.Text()
        fmt.Println(strings.TrimRight(text, "\n"))
    }
}

显然不是我要的 termios, 而且执行也报错。

$ go run ./chatlib.go smallchat-client.go  127.0.0.1 7711
Error setting raw mode: inappropriate ioctl for device
exit status 1

于是我去搜索: golang termios 得到的是:github.com/pkg/term/termios 到这里就开始走偏了。

我告诉 copilot :
把以上 smallchat-client.c 翻译成Go代码,其中 #include <termios.h> 的替代应该是 github.com/pkg/term/termios

到这里 Copilot 开始胡诌代码了,例如: term, err := termios.NewTermios(os.Stdin.Fd()) 这句代码就是属于不存在的,更本没有这个函数。

关于我和 Copilot 的对话,我都放到 github 上了,感兴趣的小伙伴可以去看看,这里不赘述。

换我来辅助

于是我还是打开了 smallchat-client.c 代码,仔细研究了一下,通过 TCP 连接上 server,然后初始化一个 termios 开始接受输入输出信息。翻译的 TCP 代码逻辑没问题,就是 terminal 这块不对。

于是我又看了下 github.com/pkg/term/termios ,那我试试 github.com/pkg/term , RawMode 这个方法就是要找的,但我又看了下 Open 方法:

Open("/dev/ttyUSB0", Speed(19200), RawMode) 

只能接受, 可读写的文件路径(string类型),而 tcp.Dial 返回的 conn 是一个 ReadWriter 对象。所以就没法用。

在网上搜索了好久,看到了 golang.org/x/term, 并且它有方法:

func NewTerminal(c io.ReadWriter, prompt string) *Terminal

那就该是它了。

马上告诉 Copilot ,让它重新生成代码。

... code ...

以上C代码转换为Go代码,其中 termios 部分,使用 golang.org/x/term 实现

马上执行试试看,

$ go run ./chatlib.go smallchat-client.go  127.0.0.1 7711
# 直接卡死。。。

好吧,生成代码失败。

回归需求

最后还是我手动完成这个 client 端。

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"

    "golang.org/x/term"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Printf("Usage: %s <host> <port>\n", os.Args[0])
        os.Exit(1)
    }

    conn, err := net.Dial("tcp", os.Args[1]+":"+os.Args[2])
    if err != nil {
        fmt.Println("Error connecting:", err)
        os.Exit(1)
    }
    defer conn.Close()

    go func() {
        scanner := bufio.NewScanner(conn)
        for scanner.Scan() {
            fmt.Println(scanner.Text())
        }
    }()

    var tty = term.NewTerminal(conn, "> ")

    // get stdin in
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        tty.Write([]byte(scanner.Text() + "\n"))
    }

    return
}

码成

完整,可运行的代码我放——https://github.com/PaulXu-cn/smallchat 了。

附上运行效果:
go_chat_show

嗯,和 c 写的差不多,(自言自语)

不过 client promot 这块功能是缺失的,目前还没搞清楚,这是和 C 版 smallchat 的差别,目前还在看 go 的API。

总结

C代码是用 fd 管理资源,而 go 作为较为现代语言,是不希望程序猿接触、管理 fd,期望用户操作对象来管理。这就是造成 两边 代码风格比较不同的原因,同时也影响了copilot生成代码,时不时出现了 Go 有操作Fd API的幻觉!

同时,在理解 temrnial 这块, copilot 它自己都没头绪,每次都是硬答,我看了下关于 Go termimal 这块的资料确实比较少,这可能导致 copilot 没有学习到如何使用 Go termnial 相关的API 吧。

好,欢迎大家留言交流。

本文参与了SegmentFault 思否写作挑战赛活动,欢迎正在阅读的你也加入。

小白要生发
1k 声望1.2k 粉丝

GoPHPer工程师