2023年3月

最近一直再看 PHP 的底层源码,同时也对如何开发一门语言来了兴趣,看了《自制编程语言》这本书,在第一个计算机的例子上就栽了跟头。

按照书上的例子写完代码后报错:

y.tab.c:1049:16: error: implicit declaration of function 'yylex' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
      yychar = yylex ();
               ^
y.tab.c:1224:7: error: implicit declaration of function 'yyerror' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
      yyerror (YY_("syntax error"));
      ^
y.tab.c:1335:3: error: implicit declaration of function 'yyerror' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
  yyerror (YY_("memory exhausted"));
  ^

最终 google 了一番发现很多人卡在这里。

https://stackoverflow.com/questions/20106574/simple-yacc-grammars-give-an-error

最终找到了这个提问解决了这个问题,其实主要是找不到定义文件,我们在定义 yacc 文件的时候,需要在头部定义一下。

%{
#include <stdio.h>
#include <stdlib.h>
#define YYDEBUG 1
int yylex(void);
int yyerror(char const *str);
%}

Rasmus Lerdorf 还做一个叫做 Form Interpreter 的项目,可以轻松地将SQL查询嵌入到网页中。它基本上是另一个CGI包装器,可以解析SQL查询,并能在这些查询的基础上轻松创建表单和表格,Rasmus Lerdorf 最终将两个项目合并到了一起,将其合并成为了一个程序,也就是 PHP/FI。现在它已经发展到了嵌入 HTML 文件的简单编程语言的程度,所以代码量飞起,直接涨了接近 20 多倍。

这个阶段的 PHP/FI 此时已经具备了 PHP 的雏形,PHP 完成从“个人主页工具”到 “个人主页建设工具包”。

2023-03-22T15:20:10.png

PHP脚本语言的语法在很多方面与C语言相似。它支持变量、数组、函数调用、不同的变量类型和大多数编写复杂的cgi程序时可能需要的东西。也就是说 PHP 编程语言正式出现了!

在PHP/FI 中引入了 yacc 和 lex 进行词法分析和语法解析,其原理其实还是通过解析 html 中的标记位,然后通过词法解析和语法解析成为C 语言代码进行执行,其核心代码在 lex.cparse.c 两个文件中。

我们先来看一下 PHP/FI 的使用,假设你有一个表单。

<FORM ACTION="/cgi-bin/php.cgi/~userid/display.html" METHOD=POST>。
<INPUT TYPE="text" name="name">
<INPUT TYPE="text" name="age"> <INPUT TYPE="text" name="age" >。
<INPUT TYPE="提交">
</FORM>

然后你的display.html文件可以包含这样的内容。

<?
    if($age>50);
        echo "Hi $name, you are ancient!<p>";
    elseif($age>30);
        echo "Hi $name, you are very old!<p>";
    else;
        echo "Hi $name.";
    endif;
>

就是这么简单! PHP/FI为表单中的每个输入字段自动创建一个变量。然后你可以在ACTION URL文件中使用这些变量。

上面的这段例子是来自于 PHP/FI 的文档,其文档地址在压缩包中也存在,目录是 doc。

2023-03-22T12:54:33.png

这个时候的 PHP/FI 还提供了 mysql 数据库:

2023-03-22T12:57:16.png

    <?
      $name = "bob";    
      $result = mysql($database,"select * from table where firstname='$name'");
      $num = mysql_numrows($result);
      echo "$num records found!<p>";
      $i=0;
      while($i<$num);
        echo mysql_result($result,$i,"lcase(fullname)");
        echo "<br>";
        echo mysql_result($result,$i,"address");
        echo "<br>";
        $i++;
      endwhile;
    >

其实到了这一步,我们的 PHP/FI 其实就已经可以完成目前我们的日常业务开发了,也就是 CURD。

同时我们可以通过 changelog 就可以看出,越来越多的人参与到了 PHP 的研发工作当中,也正是因此,PHP 逐渐壮大到了今天这个地步。

2023-03-13T18:17:10.png

目前 PHP/FI 的文档还可以访问 https://www.php.net/manual/phpfi2.php ,有兴趣的朋友可以去看一下。

源码解析

PHP 的本质其实还是 C,这也是大多数脚本语言的本质,我们可以把脚本语言的解释器理解为就是一个特殊的程序,解释器的将其中的各种语法解析、转换成可以执行的 C 代码而已。如果这么理解,那么对于脚本语言的理解就很容易了。

那么我们在定义一个变量,自定义一个函数的时候,其实解释器需要将这些变量、函数存储起来,然后在需要使用的地方进行调用。

接下来,我们就来看一下,PHP/FI中的自定义函数、变量是如何存储的。

PHP 的函数保存在哪里?

内置函数

在了解自定义函数的存储之前,我们先来看一下内置函数是如何进行处理的,在 PHP/FI 中为我们提供了大量的内置函数。

所有的内置函数都会被存储在一个变量名叫做 cmd_table 的 hashtable 中。

typedef struct _cmd_table_t {
    char *cmd;
    unsigned int token;
    void (*fnc)(void);
} cmd_table_t;

  • cmd 是函数的名称
  • token 是token 的编号
  • 最后一个成员是指向函数的指针

2023-03-22T15:29:12.png

自定义函数

2023-03-22T15:24:40.png

PHP 中的变量

支持的变量类型

支持三种类型的变量。长整数、双精度浮点和字符串。

#define    DNUMBER    258
#define    LNUMBER    259
#define    STRING    260

PHP 通过类型推断来判断变量具体的数据类型。

/*
 * Determines if 'str' is an integer, real number or a string
 *
 * Note that leading zeroes automatically force a STRING type
 */
int CheckType(char *str) {
    char *s;
    int type=LNUMBER;

    s = str;
    if(*s=='0' && *(s+1) && *(s+1)!='.') return(STRING);
    if(*s=='+' || *s=='-' || (*s>='0' && *s <='9') || *s=='.' ) {
        if(*s=='.') type=DNUMBER;
        s++;
        while(*s) {
            if(*s>='0' && *s<='9') { 
                s++; 
                continue; 
            }
            else if(*s=='.' && type==LNUMBER) { type=DNUMBER; s++; continue; }
            else return(STRING);
        }
    } else return(STRING);
    return(type);
} /* CheckType */

表达式的栈管理

/* Expression Stack */
typedef struct Stack {
    short type;
    char *strval;
    long intval;
    double douval;
    VarTree *var;    
    struct Stack *next;
    int flag;
} Stack;

变量的存储

变量的存储,在这个版本中的 PHP 存储变量使用的并不是 HashTable,而是使用的树。这说明 PHP 的设计也是在不断优化调整的过程。


/* Variable Tree */
typedef struct VarTree {
    short type;
    int count;
    char *name;
    char *strval;
    char *iname;
    long intval;
    double douval;
    int flag;
    int scope;                /* 0=local to frame, 4=global, 8=static to frame */
    struct VarTree *left;
    struct VarTree *right;
    struct VarTree *next;
    struct VarTree *prev;
    struct VarTree *lacc;
    struct VarTree *lastnode;
    int deleted;
    int allocated;
} VarTree;

这个时候的变量的值全部都是存在 tree 中的,根据 type 来判断变量的类型。

socpe 实现了变量的作用域。

参考

Personal Home Page Tools

1.0.8 是我们在官网上能够下载到的最古老的版本,虽然这个时候也叫 PHP,但是和现在的 PHP 意思是完全不同的,下面这张图是在源代码中的截图。

2023-03-13T08:49:59.png

PHP Tools - Personal Home Page Tools 个人网站工具集,严格意义上来说,这个时候的 PHP 只是一个 CGI 的工具集,它包含了一个非常简单的语法分析引擎,只能理解一些指定的宏和一些Home Page后台的常见功能,如留言本,计数器和一些其他的素材。

2023-03-22T13:01:01.png

Rasmus Lerdorf,就是这个家伙,在 1995 年仅用了一个下午就完成了 PHP 的第一个版本,对我们的生活其实产生了深远的影响。在 2004 年 PHP 更是成为了年度语言,并且在其后很长的一段时间内保持着前五,虽然近些年来 PHP 一直在被唱衰,甚至在国内大厂不断地用 Go 代替 PHP 的环境下,在 2023 年仍然没有跌出前十。

PHP 源代码下载,历史的归档源代码我们可以到 https://museum.php.net/ 这里进行下载。

2023-03-13T11:15:18.png

我们用 cloc 分析一下源代码发现,这个时候的 'PHP' 其实只有两千多行代码,其中真正用 C 语言实现的代码共计是 2455 行,等到了php2.0.1 的时候, C 语言的代码量就已经打到了进阶 58957 行。

2023-03-13T11:13:10.png

和现在的 PHP 代码行数完全不是一个量级的,这是因为在当年大多数都是静态页面,而开发 PHP 的目的就是为了统计网站的访问人数。

这个时候的 PHP 是如何执行的呢,又或者 CGI 程序是怎么回事?

用 Shell 也能写网站

这里不会跟朋友们扯太多空洞洞的理论,因为理论知识很容易就可以获取到,随便看几本书就可以了,或者去百度百科,都会有非常详细的介绍。

在上世纪 90 年代互联网刚刚起步,那个时候大家都是在用 html 搭建一个个人简历或者是公司简介,这个时代就是 web1.0 时代,很快 CGI 出现了,拜 CGI 之赐,网站不再只有固定不变的图形和文字,借由程序动态产生的网页可以让网站好象『活』了起来。小从简单的网页计数器,留言版,大至处理众多资料的搜寻引擎,可做线上实时交易的电子商务、网络下单等。CGI 简单、开放、跨平台、与程序语言独立的特性,使得编写网站应用程序变得很容易。

  1. 用户访问apache
  2. apache 调用 cgi程序,在这个过程中会将请求数据发送给 cgi 程序。
  3. cgi 输出执行业务逻辑并且输出页面。

按照这个原理,其实任何语言都可以编写 cgi 程序,下面我们用 shell 来编写一个打印 get 参数的 echo 服务。

#!/bin/bash

echo 'Status: 200 OK'
echo 'Content-Type: text/html; charset=utf-8'
echo 'Cache-Control: no-cache'
echo 'Pragma: no-cache'
echo
echo '<html>'
echo '<head>'
echo '<h1>echo query:</h1>'
echo '<p>'
echo  $QUERY_STRING
echo '</p>'
echo '</body>'
echo '</html>'
echo

2023-03-13T11:37:26.png

PHP 的使用案例

这个时候 PHP 提供了四个 cgi 程序:phpl.cgi, phpf.cgi, phplmon.cgi, phplview.cgi

其实这个时候的 PHP 就已经在简化 HTML 开发了,其中提供了一些内置函数可以帮我们进行模板替换。

<ul>
  <li>$today = <!--$today--></li>
  <li>$version = <!--$version--></li>
  <li>$_GET['name'] = <!--$name--></li>
  <li>system load:<pre><!--! w|head -n1 --></pre></li>
</ul>

<br>This page has been accessed a total of <a href="phplmon.cgi"><b>3</b></a> times now! <b>3</b> times today.<br>Page was last updated on Dec 22, 2021 at 7:43.</center></font>

2023-03-22T13:44:38.png

我们可以看到 <!--$today--> <!--$version--> 都已经被 phpl.cgi 程序给替换掉了。

PHP 源代码分析

PHP 的目录结构

这就是这个版本的 PHP 主要执行流程。我们来看一下代码目录

.
├── License
├── README
├── build
│   ├── phpf.cgi
│   ├── phpl.cgi
│   ├── phplmon.cgi
│   └── phplview.cgi
├── common.c
├── common.h
├── config.h
├── error.c
├── html_common.h
├── phpf.c
├── phpl.c
├── phplmon.c
├── phplview.c
├── post.c
├── post.h
├── subvar.c
├── version.h
├── wm.c
└── wm.h
  • common.c & common.h 一些公共函数的声明,我们看到有很多函数其实是没有.h 声明的,其实就放在了这里。
  • config.h : 相当于php.ini 用来存储 PHP 相关的配置信息
  • html_common.h && error.c :公共函数,这个名字起的让人很迷惑,里面有处理html_error 的处理函数,也有公用头部函数。
  • phpl.c
  • phplmon.c
  • phpview.c
  • phpf.c
  • subvar.c :变量替换函数
  • version.h :存放了 PHP 的版本信息,作者名字。
  • vm.c & vmc.h :字符串匹配函数,有趣的是这段代码并不是 Rasmus Lerdorf 写的,而是 COPY 过来的,并且他在代码中感谢了 Justin Slootsky。这说明无论是什么人都会有自己的短板。

在这里的 build 文件其实是不存在的,这是为了方便编译我自己就修改了 Makefile 文件。

PHPL.C 的执行流程

  1. 初始化变量
  2. 获取 Post 数据,如果有则初始化 Post 链表,如果没有则不执行
  3. 校验参数,也就是 CGI 程序发过来的 argv,校验不通过则渲染错误 html
  4. 渲染(其实就是标准输出)HTML,其中包括模板解析,替换等操作
  5. 释放Post 链表,结束程序

PHP 的配置文件

我们先来看配置文件 config.h :

define ROOTDIR "/usr/somewhere/user_id/public_html"
define HTML_DIR "html" 

#define LOGDIR "logs/"

#define ACCDIR "logs/"
#define NOACCESS "NoAccess.html"

这个时候的配置文件可以说是相当简洁了,只有四个参数,所以根本不需要独立的配置文件,我们只需要在编译之前进行修改就可以了。

  • ROOTDIR:项目根目录,在例子中,我们使用 phpl.cgi?var.html, var 就存储在根目录下,如果没有设置的话,回去找 apached 的默认配置,并且增加上 /public_html。
  • HTMLDIR:如果你不想选择使用 rootdir 也可以利用这个进行指定目录
  • LOGDIR:日志
  • ACCDIR:日志
  • NOACCESS:无法访问调转到指定页面。

PHP 的模板替换

在 phpl.c 中定义了两个宏。

#ifndef STARTSEP
#define STARTSEP "<!--"
#endif
#ifndef ENDSEP
#define ENDSEP   "-->"

这两宏的作用其实就是代表着模板替换的开始和结尾。

在这个时候,其实 PHP 有点类似于 Smarty 模板引擎,在 PHP 中预设了很多变量,当加载 HTML 的时候,如果发现 <!-- --> 模板标记,就会将其中的内容替换成已经赋值的变量

2023-03-22T13:47:28.png

我们可以看到截图中代码替换,其实就是检测到 $total 就替换成已经实现准备好的变量。

  • 在 632 和 637 行 其实就是去截取 <!-- --> 中的内容
  • 查找到对应的标记进行替换。

在 651 行中有 $today,我们以此举例。

static int todays_cnt;

在 phpl.c的最上方定义了静态变量 todays_cnt,其实该记录存储的就是今日的访问记录。

if(fgets(cntbuf,127,fp)) {
    s=strchr(cntbuf,' ');
    if(s) *s='\0';
        cnt=atoi(cntbuf);
        if(s) todays_cnt=atoi(s+1);
    } else {
    cnt=0;
    todays_cnt=0;
}

PHP 中的 Post 数据

用链表存储 Post 数据

2023-03-22T15:43:09.png

在 phpl.c 中第 88 行定义了一个全局静态变量, pdtop 其结构定义在 post.h 中。

typedef struct _postdata {
    char    var[32];
    char    val[128];
    struct _postdata *next;
} postdata;

这是一个链表,第一个 char 存储的事 post 的 key,第二个 char 存储的是值,在这里我们可以看出,在这个时候 PHP 最大只能接受 128 个字符。

获取 post 数据

在 cgi 协议中 http 请求中的数据是通过环境变量交给 cgi 程序的。

2023-03-22T15:37:12.png

Post 数据的释放

在我们申请内存后,必须对其进行释放,否则就会造成内存泄露,当 php 执行完成后会对已经申请过的内存地址进行释放。

2023-03-22T15:46:01.png

其实操作非常简单,就是判断第一个节点是否为空,如果不为空则证明有 post 的数据,然后开始遍历链表,释放掉所有申请的内存地址。

参考

这部分其实并不是 rancher 的内容,在我们之前的文章中,我们需要上传我们的可执行文件到虚拟机上,目前仅有两个 woker 节点,这样每一个节点进行上传其实还好,但如果节点变成了 10 个,20 个甚至更多的话,那么将会消耗大量的时间,虽然上产环境中基本上没有这么玩的。:)

我们完全可以利用 NFS 来共享我们的主机目录,这样一来,这样一来,我们只需要向一个节点上传文件即可。

# 安装 nfs-utils
$ yum install nfs-utils -y

# 编辑配置文件
$ sudo vim /etc/sysconfig/nfs

LOCKD_TCPPORT=30001 # TCP 端口占用
LOCKD_UDPPORT=30002 # UDP 端口占用
MOUNTD_PORT=30003 # 挂在使用端口
STATD_PORT=30004 #状态使用端口

# 启动服务
$ systemctl restart rpcbind
$ systemctl restart nfs-server
$ systemctl enable nfs-server
$ systemctl enable rpcbind

# 创建需要共享的目录
$ mkdir -p /data/nfs/

# 编写共享配置文件
$ vim /etc/exports
/data/nfs  192.168.0.0/24(rw,async)
# 重启服务
$ systemctl restart nfs-server

#查看挂载是否生效
$ showmount -e localhost
Export list for localhost:
/data/nfs 192.168.0.0/24

接下来,我们将 goapi 的可执行文件上传到到服务器上。

# 先在服务器上创建myapp
$ mkdir myapp
# 本地上传
$ scp -p build/myserverv2 [email protected]:/data/nfs/myapp/

在其他机台主机上执行如下命令

$ yum install nfs-utils -y

$ showmount -e 192.168.0.211
Export list for 192.168.0.211:
/data/nfs 192.168.0.0/24

$ mkdir -p /data/nfs
$ mount -t nfs 192.168.0.211:/data/nfs /data/nfs

$ tree /data/nfs/
/data/nfs/
└── myapp
    └── myserverv2

这样一来,我们的文件就共享出来了。

在 ClusterIP 模式下应用只能在集群内部进行访问,外部(公网)无法访问它,例如 MySQL,Redis 这些服务,外网是不许要进行访问的,这个时候就可以选择这个模式。

现在我们来模拟这个场景,例如现在访问 goapi.maksim.wbesite 现在显示的是 hellword,如果我们想在内网增加一个技术服务,显示一共有多少人访问了当前接口。

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "sync"
)

type Counter struct {
    counter int
    sync.Mutex
}

func NewCounter() *Counter {
    return &Counter{counter: 0}
}
func (receiver *Counter) Add() {
    receiver.Lock()
    defer receiver.Unlock()
    receiver.counter = receiver.counter + 1
}

func (receiver *Counter) Get() int {
    receiver.Lock()
    defer receiver.Unlock()
    return receiver.counter

}

type Response struct {
    Count int
}

func main() {
    r := gin.Default()
    counter := NewCounter()
    r.Handle("GET", "/count", func(context *gin.Context) {
        counter.Add()
        context.JSON(http.StatusOK, &Response{Count: counter.Get()})
    })
    err := r.Run(":8080")

    if err != nil {
        log.Fatalln(err)
    }
}

代码非常简单,我们继续使用前面讲到的方法将其部署的 k8s 中,在这里就不赘述了,这里主要关注网络模式选择了集群IP,并且暴露的端口号是 80。

2023-03-04T13:49:46.png

当我们部署好后,可以看到访问端口显示的n/a。

2023-03-04T13:50:07.png

我们在 master 节点上利用 curl 访问 容器的IP。

2023-03-04T13:50:37.png

我们对容器进行重新部署,这个时候你会发现,ip 地址变了,我们不可能每一次重新部署后都去关注这个 ip 的变化,所以就需要利用到服务发现了。

2023-03-04T13:50:47.png

当我们创建好 workloads 后,其实 rancher 已经帮助我们创建好了服务发现,我们点击服务发现。

2023-03-04T13:51:01.png

在其中我们可以看到 counterservice。

在这里简单说一下服务发现的机制,在 Rancher 2.4 版本以后使用 k8s-coredns 作为服务发现的基础, serivce 后会自动增加一条解析记录,如果在同一 namespace 下,我们直接可以在容器内访问 counterservice。不同空间需要利用 service_name.namespace 进行访问。

2023-03-04T13:51:14.png

我们进入my-go-api 容器中可以试验一下。

2023-03-04T13:51:34.png

这一步需要安装 curl 工具,具体方法请自查百度。这样一来就证明可以通讯了,接下来,我们改造 my-go-api。

r.Handle("GET", "/", func(context *gin.Context) {
        host := context.Query("host")
        if host == "" {
            context.JSON(400, gin.H{"error": "no host!"})
            return
        }
        resp, err := http.Get("http://" + host + "/count")
        if err != nil {
            context.JSON(400, gin.H{"error": err.Error()})
            return
        }
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            context.JSON(400, gin.H{"error": err.Error()})
            return
        }
        context.String(200, "hello world!"+string(body))
    })

我们来接收一个 host,这个 host 就是 service 名字,将改造后的代码上传至服务器,重新部署。

2023-03-04T13:51:54.png

我们直接可以点击克隆,然后新建一个叫做 demo 的命名空间,并且将名称改为counterservice2。

2023-03-04T13:52:08.png

2023-03-04T13:52:21.png

2023-03-04T13:52:31.png

当我们在浏览器内输入 counterservce2 的时候会发现报错了。

2023-03-04T13:52:40.png

我们现在输入 counterservce2.demo 就可以访问另外一个命名空间下的项目了

2023-03-04T13:53:01.png

这是服务发现的基本机制,如果不需要做健康检查的话,我们完全可以直接用 k8s 来做服务发现。