背景
这些天出现了不少好玩的ai项目,俺也是折腾的好不快乐。项目玩着玩着,发现清一色前端全是gradio写的,像SoVITS、ChatTTS啥的。gradio开箱即用,虽说和美观
大相径庭南辕北辙,但对于ai项目来说,项目能跑就行,为了前端美观不值当,有万能的社区呢。但咱今天重点不在于此,gradio在启动时如果设置下参数
share=True
,浏览器便会打开个形如https://XXX.gradio.live
的网址,你第一眼可能可能会有点懵,啥啥啥,这是个啥!鬼使神差的你访问了一下,嗨,这
玩意还真能访问,话说,它是咋把127.0.0.1夺舍的?嘿,这就得说道内网穿透了。
内网穿透
啥叫内网穿透?
我们查下百度百科,内网穿透,也即 NAT 穿透,进行 NAT 穿透是为了使具有某一个特定源 IP 地址和源端口号的数据包不被 NAT 设备屏蔽而正确路由到内网主机。
是不是感觉说的不是人话?我们翻译一下,就是在外面能访问家里的电脑。当然,gradio做的还没那么绝,只是原来这个项目只能局域网访问,现在这个限制不存在了。
这么好用的东西只能创建gradio项目使用,太暴殄天物了吧!俺也这么想的,整吧。
解决方案
我在寻求解决方案的时候,偶然发现个这个么项目gradio-tunneling,俺一拍大腿,嚯,还真是俺想要的,思路一看立马清晰明了。
刚出门便到了终点,这文章还写啥啊,按按delete吧。
我说服自己那是个python项目,很多人没python环境。脑袋里就有个人小人鄙视道,你就不能打个包,又没用啥大的库,python环境整进去能有多大,多少人电
脑上几百个浏览器了也没见抱怨。文件洁癖是病得改。
我把小人给摁住了,拿go练练手吧,命令行应用用go合适的不得了。
定义命令行
在go中命令行常用有两种,一种flag
,还有一种cobra
flag
是标准库,cobra
是第三方库,这个项目怎么说呢,cobra
是杀鸡用牛刀,flag
整呗
// 定义命令行参数
portPtr := flag.Int("port", 8085, "定义要转发的端口")
address := flag.String("address", "https://api.gradio.app/v2/tunnel-request", "分享服务器地址")
binPath := flag.String("binPath", binaryPath, "frpc程序路径,默认查找可执行文件同级的bin目录")
再设置下帮助,就是输入-h
时显示一大串参数那功能
flag.Usage = utils.PrintUsage
怎么显示呢,就把参数名是啥,功能是啥列出来
func PrintUsage() {
fmt.Fprintf(flag.CommandLine.Output(), "使用方法: %s [选项]\n", flag.CommandLine.Name())
fmt.Fprintln(flag.CommandLine.Output(), "选项:")
flag.PrintDefaults()
}
设置frpc文件路径
gradio穿透就是利用frpc来实现的,我们需要定位frpc位置,至于为啥要定位,难道只想在windows上跑?那go语言的交叉编译不是白瞎了,frpc不同平台主程序肯定不一样啊,linux的剑斩windows的官,这个想法太大胆了。
frpc存放在同级bin目录之中,我们设置一下规则名称。
func GuessFrpcBinaryName() string {
//判断当前系统平台
platform := runtime.GOOS
//架构
arch := runtime.GOARCH
return fmt.Sprintf("frpc_%s_%s", platform, arch)
}
这样我们就获取到了当前系统应该找啥路径。
binaryPath = fmt.Sprintf("bin/%s", fileName)
我们就可以对该路径该判断判断,该干嘛干嘛,当然,手动指定也是可以的
if *binPath != "" {
binaryPath = *binPath
//查询是否存在
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
log.Fatalf(fmt.Sprintf("frpc 二进制文件路径不存在: %s\n", binaryPath))
return
}
} else {
// 查询当前目录是否存在bin目录
if _, err := os.Stat("bin"); os.IsNotExist(err) {
log.Fatalf("frpc文件不存在: %s\n", binaryPath)
return
}
//查询是否存在
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
log.Fatalf("frpc文件不存在: %s\n", binaryPath)
return
}
}
//设置binaryPath为绝对路径
binaryPath, _ = filepath.Abs(binaryPath)
调用frpc还需要生成一个token
import (
"crypto/rand"
"encoding/base64"
)
func GenerateSecureToken(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
至此,万事俱备,该进入正题了
调用frpc
首先,定义一个结构体用于存放上述信息
type Tunnel struct {
proc *exec.Cmd
FrpcPath string
RemoteHost string
RemotePort int
LocalHost string
LocalPort int
ShareToken string
stdoutReader io.ReadCloser
stderrReader io.ReadCloser
}
构建命令
func (t *Tunnel) Start() (string, error) {
cmdArgs := []string{
"http",
"-n",
t.ShareToken,
"-l",
strconv.Itoa(t.LocalPort),
"-i",
t.LocalHost,
"--uc",
"--sd",
"random",
"--ue",
"--server_addr",
fmt.Sprintf("%s:%d", t.RemoteHost, t.RemotePort),
"--disable_log_color",
}
t.proc = exec.Command(t.FrpcPath, cmdArgs...)
log.Printf("cmd:%s\n", t.proc.String())
...
return url, nil
}
读取生产的公网地址
func (t *Tunnel) readURLFromTunnelStream(r io.Reader) (string, error) {
log.Println("Reading from stream...")
// Compile regex pattern once outside the loop for efficiency.
re := regexp.MustCompile(`start proxy success: (.+)`)
reader := bufio.NewReader(r)
var url string
var err error
// Setup a single-use timer for timeout.
timeout := time.After(30 * time.Second)
// Read lines from the stream with timeout.
for {
select {
case <-timeout:
log.Println("Timeout occurred while reading from stream.")
err = errors.New("read timeout")
goto exit
default:
line, readErr := reader.ReadString('\n')
if readErr != nil {
if readErr != io.EOF { // Ignore EOF which can be a normal termination signal.
err = readErr
}
goto exit
}
line = strings.TrimSpace(line) // Remove leading/trailing whitespaces.
if line == "" {
continue
}
log.Println("Read line:", line)
if strings.Contains(line, "start proxy success") {
matches := re.FindStringSubmatch(line)
if len(matches) == 2 {
url = matches[1]
goto exit
}
} else if strings.Contains(line, "login to server failed") {
err = errors.New("login to server failed")
goto exit
}
}
}
exit:
log.Println("Read operation completed.")
return url, err
}
打包
当然可以使用go build
,简单快捷,但是,不试试更爽的gox
么
gox
安装
go get github.com/mitchellh/gox
我看很多教程安装完了就直接用了,我试了识别不了,这种情况下咋办
找到gox
文件夹,找不到就git clone
吧
C:\Users\{user}\go\pkg\mod\github.com\mitchellh
运行go build
,将生成的gox.exe
添加到环境变量
gox -output "build/gradio-tunnel_{{.OS}}_{{.Arch}}"
各个平台的包就吭哧吭呲都编译好了,还在程序名上做了区分
自动化打包
但咱还得把bin目录添加进去啊,还得压缩啊,整个全自动化吧
构建这东西还是python写方便,先来个压缩
import zipfile
import os
def zip(zip_path, files):
"""
压缩文件至zip
压缩文件至zip
:param zip_path: [FILE]zip包路径-zip包路径
:param files: [DIR]压缩目录-待压缩文件的目录 字符串和字符串数组均可
"""
if isinstance(files, str):
files = [files]
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for file in files:
if os.path.isdir(file):
for dir_path, dir_names, file_names in os.walk(file):
for filename in file_names:
file_path = dir_path.replace(file, '')
zf.write(os.path.join(dir_path, filename), file_path + '\\'+Path(file).name+"\\" + filename)
elif os.path.isfile(file):
path, file_name = os.path.split(file)
zf.write(file, file_name)
else:
raise Exception('请检查路径%s' % file)
打包完将bin目录和主程序一起制作zip压缩包
def main():
project_home = "D:\xxx"
build_home = r"D:\build\xxx"
if Path(build_home).exists():
log.info("build_home exists")
# 清空
for file in Path(build_home).iterdir():
if file.is_file():
file.unlink()
log.info("delete %s success" % file.name)
else:
log.info("build_home not exists")
Path(build_home).mkdir(parents=True,exist_ok=True)
# 编译gox
result = subprocess.run(["gox","-output","%s/gradio-tunnel_{{.OS}}_{{.Arch}}" % build_home],cwd=project_home)
if result.returncode == 0:
log.info("gox compile success")
else:
log.error("gox compile failed")
# 遍历build_home下的文件
for file in Path(build_home).iterdir():
if file.is_file():
# 压缩文件
zip_path = file.with_suffix(".zip")
zip(zip_path, [file,project_home+"\\bin"], "")
log.info("zip %s success" % file.name)
# 删除文件
file.unlink()
log.info("delete %s success" % file.name)
齐活儿,下面就是各个平台测试了,精力有效,就试了试windows和linux,嗯,效果不错不错,白嫖的代理就是爽。
代码
本文代码已开源,感兴趣可参考,github创建了realease,下载即用。