前言

做毕设的时候用到了 Go 和 C++ 开发了两个 Web 后端,顺便简单记录一下。


Go - gin

Go 的函数返回中数据和错误一般是分开的,方便错误处理。Web 开发选用的是比较轻量级的 gin 框架,这个框架的中文文档好像有点时日没有更新了,建议去看 GitHub 上的官方文档,在开发过程中用到了这些库:

"net/http"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"sort"

Go 的库安装也很简单,go get 就可以了,也可以自己去 GitHub 上下载。

第一个 http 库是为了 set cookie 使用的,用来表示 cookie 的安全策略:

c.SetCookie("SESSID", string(data)[:36], 3600 * 24 * 30, "/api/", "xxx.monster", http.SameSiteLaxMode, false, true)

第二个是 json 库,大概是这样用法:

var session map[string]interface{}
json.Unmarshal([]byte(data), &session)
...
team = session["team"].(string)

data 是 json 字符串,值得一提的是从这个 session 里面取值的时候,需要加上数据类型,还有则是在我的 docker 环境(也就是 go 官方镜像)下,反序列化之后,数字类型会默认用 float64 存放,所以取值也需要用 float64 来取值。

第三个是 gin,不提。第四个是 redis 库,具体做法是初始化一个连接池,需要用的时候再取出使用:

func newPool(addr string) *redis.Pool {
    return &redis.Pool{
        MaxIdle: 3,
        IdleTimeout: 240 * time.Second,
        Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
    }
}
...
conn := pool.Get()
defer conn.Close()
encodedSession, err := redis.String(conn.Do("get", cookie))

五、六都是 MySQL 库,前面加个 _ 的意思是我们不使用包里面的成员函数,只使用构造函数,同样也是连接池的做法:

func newDb() *sqlx.DB {
    Db, _ := sqlx.Open("mysql", "root:root@tcp(172.17.0.4:3306)/ctf?charset=utf8")
    return Db
}
...
rows, _ := db.Query("select cid from challenge where challenge.mode=?;", mode)
data := []map[string]interface{}{}
for rows.Next() {
    var cidstring
    rows.Scan(&cid)
    data = append(data, row)
}
rows.Close()

最后 sort 可以实现二维数组的排序:

sort.Slice(rank, func(i, j int) bool {
    if rank[i]["score"].(float64) > rank[j]["score"].(float64) {
        return true
    }
    if rank[i]["score"].(float64) == rank[j]["score"].(float64) && rank[i]["time"].(int64) < rank[j]["time"].(int64) {
        return true
    }
    return false
})

C++ - cinatra

C++ 找不到什么合适的框架,最后选用了 Cinatra,但是这个框架还不成熟,文档也缺失得不行。没事还是不要用 C++ 写 Web 后端了。

Redis 使用 hiredis:

struct timeval timeout = {1, 500000};
redis = redisConnectWithTimeout("172.17.0.2", 6379, timeout);
if (redis -> err) {

}
...
redisReply *reply;
reply = (redisReply *)redisCommand(redis, "GET %s", key.c_str());
if (reply == NULL) {
    redisFree(redis);
    redis = NULL;
    ...
}else if (reply -> len <= 0) {
    freeReplyObject(reply);
    ...
}
.......
    else if (strcmp(reply -> str, "OK") == 0) {
    freeReplyObject(reply);
    return "OK";
}
std::string result = reply -> str;
freeReplyObject(reply);

MySQL 的写法跟 PHP 很类似,自行过滤要这么做:

char *escape_team = new char[team.length()];
char *escape_password = new char[password.length()];
mysql_real_escape_string(&mysql, escape_team, team.data(), team.length());
mysql_real_escape_string(&mysql, escape_password, password.data(), password.length());
std::string str_sql = "";
str_sql += "select uid, mail from user where team='";
str_sql += escape_team;
str_sql += "' and password='";
str_sql += escape_password;
str_sql += "';";

C++ 里面还有一种类型叫做 string_view,是 string 的一个只读视图,可以提高字符串处理性能,Cinatra 这个框架在解析 HTTP 的时候就大量用到了这个类型,从 string_view 变成 string 可以这么做:

std::string(cookies["SESSID"].data(), cookies["SESSID"].length())

C++ 里面的变长数组可以使用 vector:

std::vector<Rank::Rank> ranks;

C++ 里面没有 Go 那种可以用来表示任何数据类型的 interface,所以无法制作多种成员类型的 map。使用自定义 struct 可以解决,不过这样一来,json 就无法识别我们自己设定的结构体,所以需要给它加上序列化函数:

namespace Rank {
    struct Rank {
        int uid;
        std::string team;
        long long time;
        double score;
    };
    void to_json(nlohmann::json& j, const Rank& p) {
        j = nlohmann::json {
            {"uid", p.uid},
            {"time", p.time},
            {"team", p.team},
            {"score", p.score},
        };
    }
}

自定义结构体数组的排序跟 Go 差不多,sort 调用自定义函数。

编译:

g++ main.cpp -std=c++17 -lboost_system -lpthread -lstdc++fs -luuid -lhiredis -I/usr/include/mariadb -lmysqlclient -o web

Orz


Web C++ Go

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2019-11043 Nginx+php-fpm RCE漏洞复现
VueElectronElementUI开发自己的B站弹幕姬