经过前几篇的文章,大家应该都能预感到一定要讲解 base64 函数的内容了。是的,马上要到程序登录的代码,base64 是必须要实现的。

base64 很早以前我就接触了,在项目中也很喜欢用。但每换一个新语言我总是很害怕,很排斥用它。这主要是缘于曾经的经历:多年前 base64 还没有这样普及,为了在 java 中使用 base64 找了很多的代码,虽然 jdk 中是有,但那是 sun 专用的,在网上找倒是很多,但要自己改造一下是非常的困难(其实是 java 的语言特性导致很复杂,base64 本身还是很简单的)。因为找了很多的文档一直都似懂非懂,因为不懂嘛,所以心理上就有排斥。直到我接了一个 xmpp 的项目必须要在纯 C 中实现 base64 时仔细研究了一下,其实是很简单的。说实在的关于 base64 的文章和前面提到的多线程文章一样也挺汗牛充栋的,为什么我不讲解多线程,要特别讲解 base64 呢?

因为我觉得网上的 base64 讲解文章基本上都是没用的!所以我一定要再写一篇。而我这篇,我保证您一定能看得懂!

首先我先给出完整代码,大家也可以到 github 地址去下载:
https://github.com/clqsrc/c_lib_lstring/tree/master/base64

核心的代码直接就来自博客园的博友,在 rfc 文档和其他一些地方也很容易找到。我直接再贴一个 C++ 的版本,这些代码我都默认折叠了,因为先看代码你是学不会的。

//算法来自 http://www.cnblogs.com/IwAdream/p/6088283.html, 加了我自己的注释
//基本上就是靠移位算法将 3 个字节变成 4 个字节,或者将 4 个字节变成 三个字节
//可以看 https://zh.wikipedia.org/wiki/Base64 中的表格图示
//图片可以在本源码中附带的 base64.png 中看到[如果转载的网友也转载有图片的话就会有]
//其实一张图就能明白它的原理
//把变量提前以便更多编译器支持 #ifndef _BASE64_H_
#define _BASE64_H_ #include <stdio.h>
#include <stdint.h> //clq 这个其实也不是必须的
#include <string.h>
#include <malloc.h> //clq 可以不用,有时会冲突 char base64_table[] = {
'A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z','a','b','c','d',
'e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x',
'y','z','','','','','','','','',
'','','+', '/', '\0'
}; void base64_map(uint8_t *in_block, int len) {
int i = ;
for(i = ; i < len; ++i) {
in_block[i] = base64_table[in_block[i]];
//printf("%d %c",in_block[i], base64_table[in_block[i]]);
}
if(len % == )
in_block[len] = '=';
else if(len % == )
in_block[len] = in_block[len+] = '=';
return ;
} void base64_unmap(char *in_block) {
int i;
char *c;
int decode_count = ; for(i = ; i < ; ++i) {
c = in_block + i; if(*c>='A' && *c<='Z') {
*c -= 'A';
continue;
} if(*c>='a' && *c<='z') {
*c -= 'a';
*c += ;
continue;
} if(*c == '+') {
*c = ;
continue;
} if(*c == '/') {
*c = ;
continue;
} if(*c == '=') {
*c = ;
continue;
} *c -= '';
*c += ;
}
} int _base64_encode(char *in, int inlen, uint8_t *out) {
char *in_block;
uint8_t *out_block;
char temp[];
int outlen = ; //clq add 加一个解码后的数据长度
int i = ; out_block = out;
in_block = in; for(i = ; i < inlen; i += ) {
memset(temp, , );
memcpy(temp, in_block, i + < inlen ? : inlen - i);
memset(out_block, , );
//memset(out_block, '=', 4); //好象也不用 out_block[] = (temp[] >> ) & 0x3f;
out_block[] = ((temp[] << ) & 0x30) | ((temp[] >> ) & 0x0f);
out_block[] = ((temp[] << ) & 0x3c) | ((temp[] >> ) & 0x03);
out_block[] = (temp[]) & 0x3f;
//printf("%.2x %.2x %.2x\n", temp[0], temp[1], temp[2]);
//printf("%.2x %.2x %.2x %.2x\n", out_block[0], out_block[1], out_block[2], out_block[3]);
out_block += ;
in_block += ; outlen += ; //clq add 加一个编码后的数据长度
} base64_map(out, ((inlen * ) - ) / + ); return outlen;
} int _base64_decode(char *in, int inlen, uint8_t *out) {
char *in_block;
uint8_t *out_block;
char temp[];
int outlen = ; //clq add 加一个解码后的数据长度
int i = ; out_block = out;
in_block = in; for(i = ; i < inlen; i += ) {
if(*in_block == '=')
return ;
memcpy(temp, in_block, );
memset(out_block, , );
base64_unmap(temp); out_block[] = ((temp[]<<) & 0xfc) | ((temp[]>>) & );
out_block[] = ((temp[]<<) & 0xf0) | ((temp[]>>) & 0xf);
out_block[] = ((temp[]<<) & 0xc0) | ((temp[] ) & 0x3f); out_block += ;
in_block +=; outlen += ; //clq add 加一个解码后的数据长度//这个长度其实不对,因为不一定是 3 的倍数
//if (temp[3] == '=') outlen -= 1; //clq add 有一个等号就表示补充了一个字节//优化放到最后去好了
//if (temp[3] == '=') outlen -= 1; //clq add 有一个等号就表示补充了一个字节//优化放到最后去好了 } if (in[inlen-] == '=') outlen -= ; //clq add 有一个等号就表示补充了一个字节//优化放到最后去好了
if (in[inlen-] == '=') outlen -= ; //clq add 有一个等号就表示补充了一个字节//优化放到最后去好了 //return 0;
return outlen; //clq add 加一个解码后的数据长度
} /*
int main2() { char cipher_text[64];
int i = 0; while(scanf("%s", cipher_text) != EOF) {
printf("%s\n", cipher_text); uint8_t *tran_cipher = (uint8_t *)malloc(sizeof(uint8_t) * 64);
memset(tran_cipher, 0, sizeof(uint8_t) * 64); #define ENCODE
#define DECODE
#ifdef ENCODE
printf("----------------ENCODE-----------------");
base64_encode(cipher_text, strlen(cipher_text), tran_cipher);
int len = (strlen(cipher_text) * 4 - 1) / 3 + 1;
len = len % 4 == 3 ? len + 1 : len + 2;
for(i = 0; i < len ; ++i)
printf("%c", tran_cipher[i]);
printf("\n"); #endif // ENCODE #ifdef DECODE
printf("----------------DECODE-----------------");
base64_decode(cipher_text, strlen(cipher_text), tran_cipher);
len = strlen(cipher_text);
int n = len;
while(cipher_text[--n] == '=')
;
if(n == len-2)
len = (len >> 2) * 3 - 1;
else if(n == len-3)
len = (len >> 2) * 3 - 2;
else if(n == len-1)
len = (len >> 2) * 3; for(i = 0; i < len; ++i)
printf("%c", tran_cipher[i]);
printf("\n");
#endif // DECODE
}
return 0;
} */ #endif
/*
base64.cpp and base64.h Copyright (C) 2004-2008 René Nyffenegger This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software. Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions: 1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code. 3. This notice may not be removed or altered from any source distribution. René Nyffenegger rene.nyffenegger@adp-gmbh.ch */ #include "stdafx.h" #include "base64.h"
#include <iostream> static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/"; static inline bool is_base64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
} std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) {
std::string ret;
int i = ;
int j = ;
unsigned char char_array_3[];
unsigned char char_array_4[]; while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == ) {
char_array_4[] = (char_array_3[] & 0xfc) >> ;
char_array_4[] = ((char_array_3[] & 0x03) << ) + ((char_array_3[] & 0xf0) >> );
char_array_4[] = ((char_array_3[] & 0x0f) << ) + ((char_array_3[] & 0xc0) >> );
char_array_4[] = char_array_3[] & 0x3f; for(i = ; (i <) ; i++)
ret += base64_chars[char_array_4[i]];
i = ;
}
} if (i)
{
for(j = i; j < ; j++)
char_array_3[j] = '\0'; char_array_4[] = (char_array_3[] & 0xfc) >> ;
char_array_4[] = ((char_array_3[] & 0x03) << ) + ((char_array_3[] & 0xf0) >> );
char_array_4[] = ((char_array_3[] & 0x0f) << ) + ((char_array_3[] & 0xc0) >> );
char_array_4[] = char_array_3[] & 0x3f; for (j = ; (j < i + ); j++)
ret += base64_chars[char_array_4[j]]; while((i++ < ))
ret += '='; } return ret; } std::string base64_decode(std::string const& encoded_string)
{
//--------------------------------------------------
//clq add
//--------------------------------------------------
int in_len = encoded_string.size();
int i = ;
int j = ;
int in_ = ;
unsigned char char_array_4[], char_array_3[];
std::string ret; while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
char_array_4[i++] = encoded_string[in_]; in_++;
if (i ==) {
for (i = ; i <; i++)
char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[] = (char_array_4[] << ) + ((char_array_4[] & 0x30) >> );
char_array_3[] = ((char_array_4[] & 0xf) << ) + ((char_array_4[] & 0x3c) >> );
char_array_3[] = ((char_array_4[] & 0x3) << ) + char_array_4[]; for (i = ; (i < ); i++)
ret += char_array_3[i];
i = ;
}
} if (i) {
for (j = i; j <; j++)
char_array_4[j] = ; for (j = ; j <; j++)
char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[] = (char_array_4[] << ) + ((char_array_4[] & 0x30) >> );
char_array_3[] = ((char_array_4[] & 0xf) << ) + ((char_array_4[] & 0x3c) >> );
char_array_3[] = ((char_array_4[] & 0x3) << ) + char_array_4[]; for (j = ; (j < i - ); j++) ret += char_array_3[j];
} return ret;
}

java 里一直到 jdk1.6 才有正式的 base64 函数,ios 的开发工具 xcode 的支持也很晚,搞得到处都是各种 base64 的讲解和第三方代码,甚至我最喜欢的 delphi 里都有不同的控件实现了不同的版本。给人的印象就成了这东西一定很难啊,没事别去碰。
    所以我一语道破天机的话,接触过 base64 的网友一定会跌破一地的眼镜:其实 base64 的核心算法只有 4 行代码! 是的就是只需要 4 行! 其他的代码不过是补充字节不足,找数字和字符串进行映射转换而已。

base64 其实就是将 3 个字节的字符串变成 4 个字节的字符串。就这么简单。然后再规定了一下字符串不够 3 个字节的话怎样补充而已。至于超过 3 字节的字符串嘛,是个人都明白:分割成多个 3 字节的循环就可以了。至于解码,反过来 4 字节的结果变成 3 个原文的就行了呗,所以根本不用讲解解码,我们讲解编码过程就行了。

讲解编码过程时先说下原因大家就更好理解,base64 产生的原因是要将文件中的二进制内容以可见的字符串发送出去(或者是类似的地方传输)。为什么要转换成可见的字符呢?大家看到前面的内容应该都知道网络协议基本上就是一些命令字符串了吧。那么假如要传输的内容有 '\0' '\1' '\2' 这样的内容显然就不行了嘛。所以想个办法把它们变成 'a' 'b' 'c' 就行了嘛。全部的字符嘛有 256 个,需要 8 个字节来表示(2的8次方嘛),可见的字符没有这么多,全部一对一的肯定不行。我们假设只有 64 个可见字符,怎样去表示这 256 个实际字符呢。很简单,每个字节是8位的,所以需要 256 个字符来表示才够,那把它变成每个字节 6 位的(2的6次方是64)不就行了吗。用二进制的模拟一下大家会更深清楚这个算法:
----------------
1|2|3|4|5|6|7|8|
----------------
=>
-------------
|1|2|3|4|5|6|
-------------

用计算机术语来说相当于将 8 位机换成了 6 位机。但是因为我们现在都是 8 位机(当然实际是32位或者64位,8位数据只是兼容而已)。所以变通一下,用8位地址表示 6 位的数据就行了,这样的话前两位就固定为 0 ,而数据向右移动一下就可以了,如下图:
----------------
1|2|3|4|5|6|7|8|
----------------
=>
----------------
||1|2|3|4|5|6|
----------------

好了,这就是 base64 算法!这也是为什么所有的 base64 编码算法都有移位运算符。

等等,你会说,那第7、8位置上的数据岂不是丢失了。这个好办啊,放到下一个字节上就行了呗。4个保存6位数据的字节刚好可以保存住3个保存8位数据的字节嘛(因为它们都能表示24位二进制数据,3x8=24 4x6=24 嘛)。这个"巧合"应该也是前人选择用 64 个可见字符而不是其他数目的原因之一吧。用图形表示那真是再明白不过了:

---------------- ---------------- ----------------
1|2|3|4|5|6|7|8| 1|2|3|4|5|6|7|8| 1|2|3|4|5|6|7|8|
---------------- ---------------- ----------------
=>
---------------- ---------------- ---------------- ----------------
0|0|1|2|3|4|5|6| 0|0|7|8|1|2|3|4| 0|0|5|6|7|8|1|2| 0|0|3|4|5|6|7|8|
---------------- ---------------- ---------------- ----------------

在 base64 中的 wiki 文档中也有类似的图形,实际上我上面的图示就是受了 wiki 的启发。如图:

上面的文字表格可能排版后不太好看,我再传张图片吧:

移位操作用 java 来讲解就太不合适了,所以我们只讲解 C 语言的代码,更直观。代码中唯一要注意的是队了第一个字节可以直接通过移位来得到外,其他三个字节的数据,都要从源数据的两个字节中同时得到一部分"组合"起来(算法如此,实际上第四个字节刚好只来自于源数据的第三个字节的后半部分)。组合两个个字节的运算大家在大学应该都学过了,那就是二进制的 "或" 操作。所以这篇文章虽然我自认为应该人人都能懂,不过也要读者上过基本的计算机原理课程。所以大学的教育并不是很多程序员认为的那样不实用 -- 那些都是有用的基础。在后面的文章中大家还会看到需要大学课程内容的部分。我知道有很多程序员是非科班的,那确实应该用业余时间内自己补充一下专业课程。要不确实有很多东西是不易理解的。比如没学过汇编语言并实机操作的话恐怕是永远也理解不了指针的。当然优秀的程序员与是否科班出身没有什么关系。

有了以上的知识,再来看看算法的核心,基本上就是不用讲解了(当然了我们还是要再讲解一下)。
核心的代码非常的简单,就象我们开始说的那样,只要4句话:

        out_block[] = (temp[] >> ) & 0x3f;
out_block[] = ((temp[] << ) & 0x30) | ((temp[] >> ) & 0x0f);
out_block[] = ((temp[] << ) & 0x3c) | ((temp[] >> ) & 0x03);
out_block[] = (temp[]) & 0x3f;

除了移位和 "或" 操作外,还用了 "与" 操作来去掉无关的数据位。与 0x3f 进行 "与" 操作是为了将前两位置成 0 。与其他数字进行 "与" 操作的作用类似,只是要置 0 的位置不同罢了。
结合前面的图示,这4句话的意思如下:
1.第一个结果字节直接来自原文第一个字节移位,再保证前两位是空就行了;
2.第二个结果字节由原文第一个字节和第二个字节的各一部分组成;
3.第三个结果字节由原文第二个字节和第三个字节的各一部分组成;
4.第四个结果字节直接来自原文第三个字节,并且不用移位,再保证前两位是空就行了.

另外一份的 C++ 代码,算法思想也是一样的,只是处理方法略有差异而已,大家有兴趣可以自己分析。具体如下:

    char_array_4[] = (char_array_3[] & 0xfc) >> ;
char_array_4[] = ((char_array_3[] & 0x03) << ) + ((char_array_3[] & 0xf0) >> );
char_array_4[] = ((char_array_3[] & 0x0f) << ) + ((char_array_3[] & 0xc0) >> );
char_array_4[] = char_array_3[] & 0x3f;

我个人觉得前面的 C 代码更清晰直接,所以用 C 的代码进行的讲解。其他补位、字符映射、变种什么的实在不用说了,明白了前面的算法一通百通,大家可以自己去看 wiki 或者其他文章,wiki 的链接为:
https://zh.wikipedia.org/wiki/Base64

有了这部分代码,电子邮箱的登录过程就易如反掌。具体可以到文末链接下载,核心代码非常简单:

    //用 base64 登录
s = NewString("AUTH LOGIN\r\n", m);
SendBuf(gSo, s->str, s->len); rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:%s\r\n", rs->str); s = NewString("test1@newbt.net", m); //要换成你的用户名,注意 163 邮箱的话不要带后面的 @域名 部分
s = base64_encode(s);
LString_AppendConst(s,"\r\n");
SendBuf(gSo, s->str, s->len); rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:%s\r\n", rs->str); s = NewString("", m); //要换成您的密码
s = base64_encode(s);
LString_AppendConst(s,"\r\n");
SendBuf(gSo, s->str, s->len); rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:%s\r\n", rs->str);

为了避免大家误以为 lstring 很庞杂,所以我特意把 base64 的代码放到一个独立目录,实际写程序时最好还是放到同一个目录中比较方便.本章节的完整源码可以到以下地址下载(附带了那份 C++ 的代码,实际上并不需要编译它):
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_10

(成功登录的运行截图)

--------------------------------------------------

版权声明:

本系列文章已授权百家号 "clq的程序员学前班" . 文章编排上略有差异.

一步一步从原理跟我学邮件收取及发送 10.四句代码说清base64的更多相关文章

  1. 一步一步从原理跟我学邮件收取及发送 2.邮箱的登录和绕不开的base64

    一步一步从原理跟我学邮件收取及发送 2.邮箱的登录和绕不开的base64 好了,经过本系列上一篇文章 "1.网络命令的发送",假设大家已经掌握了 email 电子邮件的命令发送的方 ...

  2. 一步一步从原理跟我学邮件收取及发送 5.C语言的socket示例

    说到 C 语言版本的程序,首先要解决的问题就是兼容性. 作为 20 年开发有 10 多年是在服务端的程序员,我深刻地感受到服务端平台的两极分化之严重,linux 派对 windows 那是超级的不屑一 ...

  3. 一步一步从原理跟我学邮件收取及发送 3.telnet命令行发一封信

    首先要感谢博客园管理员的及时回复,本系列的第二篇文章得以恢复到首页,这是对作者的莫大鼓励.说实在的本来我真的挺受打击的.好在管理员说只是排版上有些问题,要用代码块修饰下相关的信息.说来惭愧因为常年编码 ...

  4. 一步一步从原理跟我学邮件收取及发送 4.不同平台下的socket

    既然是面向程序员的文章那当然不能只说说原理,一定要有实际动手的操作.    其实作为我个人的经历来说,对于网络编程,这是最重要的一章! 作为一位混迹业内近20年的快退休的程序员,我学习过很多的开发语言 ...

  5. 一步一步从原理跟我学邮件收取及发送 8.EHLO 命令详解

    我们在上一篇中解决了接收一行命令的问题后,就可以来具体的分析邮件发送过程中涉及到的 SMTP 协议内容了. 首先来看通讯过程中的第一个内容:服务器在客户端连接上来后会主动发送一个问好的信息,所以这第一 ...

  6. 一步一步从原理跟我学邮件收取及发送 11.完整的发送示例与go语言

    经过了这个系列的前几篇文章的学习,现在要写出一个完整的 smtp 邮件发送过程简直易如反掌.    例如我们可以轻松地写出以下的纯 C 语言代码(引用的其他C语言文件请看文末的 github 地址): ...

  7. 一步一步从原理跟我学邮件收取及发送 9.多行结果与socket的阻塞

    前几篇的文章发表后,有网友留言说没有涉及到阻塞的问题吗?在 socket 的编程当中,这确实是个很重要的问题.结合目前我们文章的内容进度,我们来看看为什么说阻塞概念很重要. 接着上篇的内容,当我们发送 ...

  8. 一步一步从原理跟我学邮件收取及发送 12.telnet命令行收一封信pop3

    本系列上一篇文章中我们就说到了,这一次我们要说 pop3 收信了.虽然我觉得应该先说完 mime 格式,不过估计大家已经不耐烦了 -- 怎么老在说发送啊?我们要看收取!    好吧,来啦,来啦!收取邮 ...

  9. 一步一步从原理跟我学邮件收取及发送 13.mime格式与常见字符编码

    在前面的本系列文章中我们已经学会了邮件的发送和收取.但在收取中我们看到的是一串串的乱码,回忆前面的发送过程,我们会奇怪:我们前面的邮件是明文啊.为什么明文的邮件明明也可以正常工作,还要弄乱码似的字符串 ...

随机推荐

  1. c/s与b/s 动态网站与静态网站 (网站编码统一“UTF-8”)

    1.c/s和b/s 第一张图是b/s  可以随时随地的浏览  (在服务器增加网页就能增加功能,只要改变网页就能使用户同步更新,共享性也强,开发也简单,在广域网和局域网都能建造b/s结构,然后通过int ...

  2. 51 nod 1297 管理二叉树

    原题链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1297 先是暴力加优化T了最后两个点…… 我还是来想想正解吧. ...

  3. POJ 3154 Graveyard【多解,数论,贪心】

    Graveyard Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 1707   Accepted: 860   Specia ...

  4. UESTC 1584 Washi与Sonochi的约定【树状数组裸题+排序】

    题目链接:UESTC 1584 Washi与Sonochi的约定 题意:在二维平面上,某个点的ranked被定义为x坐标不大于其x坐标,且y坐标不大于其y坐标的怪物的数量.(不含其自身),要求输出n行 ...

  5. 蓝桥杯模拟赛-引爆炸弹-DFS+并查集

    今天整理电脑,翻出来了很久以前大佬给的题,贴一下. 引爆炸弹 1000ms 在一个 n×m的方格地图上,某些方格上放置着炸弹.手动引爆一个炸弹以后,炸弹会把炸弹所在的行和列上的所有炸弹引爆,被引爆的炸 ...

  6. HDU2988-Dark roads,HDU1233-还是畅通工程-最小生成树

    最小生成树: 中文名 最小生成树 外文名 Minimum Spanning Tree,MST 一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的 ...

  7. 备份的一些小tip

    // npm官方镜像 npm config set registry=http://registry.npmjs.org // 亚马逊S3 host 219.76.4.4 github-cloud.s ...

  8. 解决在SecurecCRT登录后,发现方向键、backspace(退格键)、delete(删除键)为乱码的问题

    问题:使用securecrt ssh到linux之后,backspace(退格键),delete(删除键),以及4个方向键都为乱码,不能正常使用.按tab键也没有自动补全文件名. 即: 按Backsp ...

  9. Hadoop2.7.5+Hbase1.4.0完全分布式

    Hadoop2.7.5+Hbase1.4.0完全分布式一.在介绍完全分布式之前先给初学者推荐两本书:<Hbase权威指南>偏理论<Hbase实战>实战多一些 二.在安装完全分布 ...

  10. SQL强化(三) 自定义函数

    ---恢复内容开始--- Oracle中我们可以通过自定义函数去做一些逻辑判断,这样可以减少查询语句,提高开发效率 create  -- 创建自定义函数 or replace -- 有同名函数就替换, ...