示例项目

实现URL缩短服务的最终版本

在URL缩短服务的最终版本中,我将做两个主要的改变:

服务器

我将在这个项目中使用的服务器是基于我在上一节课中展示的CGI服务器示例的代码。前面的示例演示了如何设置服务器和CGI赢博体育程序来处理POST请求。在这个版本的服务器中,我们将使用两个CGI赢博体育程序,一个处理POST,另一个处理GET。

预过滤GET请求

我要介绍的另一个小变化是过滤器的使用。在web服务器中,过滤器是在服务请求之前以某种方式修改请求的一段代码。在URL缩短赢博体育程序的情况下,我们需要对其中一个用例的URL做一个小的更改。当用户使用缩短服务时,他们将发送带有该形式url的GET请求

http:// <服务器地址> / s / XXXXXX

其中XXXXXX是他们想要的URL的六个字符代码。由于我们要将这个GET请求传递给CGI赢博体育程序进行处理,因此我们必须首先重写URL,将其置于更标准的GET请求形式中。修改后的表格将是

http:// <服务器地址> / cgi /解码?代码= XXXXXX

这是包含查询参数的GET请求的标准表单。query参数是URL中出现在?之后的部分。URL中的字符。

为了进行这个修改,我将在web服务器上增加以下预过滤功能。

void prefilter(char* method,char* url) {if(strcmp(method,"GET") == 0 && strncmp(url,"/s/",3) == 0) {char newURL[128];sprintf (newURL“/ cgi /解码?代码= % s”,url + 3);strcpy (url, newURL);}}

处理请求

下面是修改后的服务器中serverrequest()函数的代码:

无效serverrequest (int fd) {char lineBuffer[256];//读取请求的第一行readLine(fd,lineBuffer,255);//获取方法和URL char方法[16];char url [128];sscanf (lineBuffer、“% s % s”方法,url);预滤器(方法、url);if(strcmp(method,"POST") == 0) {if(strncmp(url,"/cgi/",5) == 0){//读取赢博体育内容直到空白行。//获取内容长度。char contentLength [16];while(1) {readLine(fd,lineBuffer,255);if(strncmp(lineBuffer,"Content-Length:",15) == 0) {strcpy(contentLength,lineBuffer+16);} else if(lineBuffer[0] == '\r') break;} if (fork() == 0) {char* emptylist[] = {NULL};setenv("CONTENT_LENGTH", contentLength, 1);dup2 (fd, STDIN_FILENO);dup2 (fd, STDOUT_FILENO);执行(url+1,空列表,环境);}等(空);} else {handle404(fd);其他}}{如果(strncmp (url,“/ cgi / 5) = = 0){如果(fork () = = 0) {char * emptylist[] ={零};char* queryPtr = strstr(url+5,"?");字符查询[32];如果(queryPtr == NULL){查询[0]= '\0';} else {strncpy(query,queryPtr+1,32);*queryPtr = '\0';} setenv("QUERY_STRING", query,strlen(query));dup2 (fd, STDIN_FILENO);dup2 (fd, STDOUT_FILENO);执行(url+1,空列表,环境);}等(空);} else{//尝试获取用户想要的文件char fileName[128];strcpy(文件名,“www”);strcat(文件名、url);int file = open(fileName,O_RDONLY);If (file == -1) {handle404(fd);} else {const char* responseStatus = "HTTP/1.1 200 OK\n";const char* responseOther = “连接:关闭\nContent-Type: text/html\n”;//获取文件char len[64]的大小;Struct stat;fstat(提交、办法);sprintf(len,"Content-Length: %d\n\n",(int) st.st_size);//发送报头write(fd,responseStatus,strlen(responseStatus));写(fd, responseOther strlen (responseOther));写(fd, len strlen (len));//发送文件字符缓冲区[1024];int bytesRead;while(bytesRead = read(file,buffer,1023)) {write(fd,buffer,bytesRead);}关闭(提交);}}} close(fd);}

对于web服务器来说,这是相当通用的代码。web服务器将把赢博体育url以/cgi/开头的请求路由到cgi赢博体育程序,并将尝试正常服务赢博体育其他请求。注意,在从请求的第一行读取方法和URL之后,对prefilter()函数的调用。

处理POST CGI请求的代码基本上与我在前面的示例中使用的代码相同。这里处理GET CGI请求的代码是新的。处理CGI GET请求时唯一的小区别是URL中出现在?字符存储在QUERY_STRING环境变量中。由于GET请求不包含主体,因此CGI赢博体育程序将简单地从QUERY_STRING环境变量中读取查询的详细信息。

数据库

我们将使用SQLite数据库系统来存储url。这是一个非常简单、轻量级的SQL数据库系统,通过一个C库实现,我们可以将其链接到我们的赢博体育程序中。在Linux中设置SQLite非常简单。我们所要做的就是在终端上运行以下命令来安装必要的软件包:

Sudo apt install libsqlite3-dev Sudo apt install sqlitebrowser

sqlitebrowser包安装了一个赢博体育程序,该赢博体育程序允许您设置数据库并查看其内容。

我们将在这个项目中使用的数据库包含在课程讲稿顶部的项目文件中。数据库存储在CGI文件夹中的SQLite .db文件中。您可以在sqlite浏览器赢博体育程序中打开这个数据库文件,以查看数据库的结构和内容。该数据库由一个表Urls组成,它有两列。id列是每个URL的整数id号。该列被设置为表的主键,并且还具有自动递增选项集。每当我们向表中插入一个新的URL时,SQLite都会自动为它生成一个整数id。URL表中的第二列是URL列,用于存储URL。

编码CGI赢博体育程序

URL缩短服务的第一步是让用户填写请求缩短URL的表单。下面是包含这个表单的页面的HTML代码:

<!DOCTYPE html> <head> <title>URL缩短服务</title> </head> <body> <h1>URL缩短服务</h1> <form action="http://localhost:8888/cgi/encode" method="Post"> <label for=" URL ">URL:</label> <input type="text" id=" URL " name=" URL "><br> <input type="submit" value=" submit" >< /form> </body>

表单的action属性会将此请求路由到encode CGI赢博体育程序。该赢博体育程序被设计为从表单中读取请求的URL,将URL存储在数据库中,然后使用包含缩短URL的网页进行响应。

这里现在是编码CGI赢博体育程序的代码:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sqlite3.h> #include <base64/base64.h> int fromHex(char ch) {if(ch >= '0' && ch <= '9') return (int) ch - '0';return (int) ch - 'A' + 10;}无效decodeURL (char * src, char * dest){虽然(* src ! = ' \ 0 '){如果(* src  == '%') { ++ src;int n1 = fromHex(*src++);int n2 = fromHex(*src++);*dest++ = (char) n1*16+n2;} else {*dest++ = *src++;}} *dest = '\0';} int main(void) {sqlite3 *db;Int rc = sqlite3_open("cgi/urls.db", &db);char * lengthStr;if ((rc == SQLITE_OK) && ((lengthStr = getenv("CONTENT_LENGTH")) != NULL)){//获取内容长度int length;sscanf (lengthStr,“% d”,长度);//从stdin char buffer[256]读取查询;读(STDIN_FILENO、缓冲、长度);Buffer [length] = '\0';//从查询字符串中分离url。Char * url = buffer+4;decodeURL (url,缓冲);//在数据库中存储URL char *err_msg = 0;char sql [128];sprintf(sql,"INSERT INTO Urls(URL) VALUES('%s');",buffer);rc = sqlite3_exec(db, sql, NULL, 0, &err_msg);//获取插入表项的id号sqlite3_int64 id = sqlite3_last_insert_rowid(db);//对id号进行编码;Encode ((unsigned int) id,code);//使响应体的字符内容[1024];sprintf(内容、“< !DOCTYPE html > \ r \ n”);sprintf(内容、“% s <头> \ r \ n”,内容);sprintf(内容、“% s <标题> URL缩短< /标题> \ r \ n”,内容);sprintf(内容、“% s < /头> \ r \ n”,内容);sprintf(内容、“% s <身体> \ r \ n”,内容);sprintf(内容、“% s < h1 >你的URL < / h1 > \ r \ n”,内容);sprintf(content,"%s<p>你的URL是http://localhost:8888/s/%s<\p>\r\n",content,code);sprintf(内容、“% s < / >身体”,内容);//发送响应printf("HTTP/1.1 200 OK\r\n");printf("连接:关闭\ r \ n ");printf(" content -length: %d\r\n", (int)strlen(content));printf (" content - type: text / html \ r \ n \ r \ n”);printf(“% s”、内容);。fflush (stdout);} else{//返回一个错误响应printf("HTTP/1.1 500内部服务器错误\r\n");printf("连接:关闭\ r \ n ");printf("内容长度:21 \ r \ n ");printf("内容类型:文本/平原\ r \ n \ r \ n”);printf(“出错了”);。fflush (stdout);} sqlite3_close (db);返回0;}

正如我们在前面的讲座中看到的CGI POST赢博体育程序的前面的例子一样,这个赢博体育程序将从CONTENT_LENGTH环境变量中读取请求的长度,然后从标准输入中读取请求。将请求的URL存储在数据库中之后,赢博体育程序将通过将应答页打印到标准输出来进行响应。

本例中的新内容是数据库交互。为了能够与SQLite数据库通信,我们需要使用SQLite C库。这样做的第一步是包含适当的头文件:

# include < sqlite3.h >

在CGI赢博体育程序的makefile中,您还将看到我将这个库链接到两个CGI赢博体育程序中。

与数据库文件交互的第一步是打开到数据库的连接:

sqlite3 *数据库;Int rc = sqlite3_open("cgi/urls.db", &db);

sqlite3_open()函数建立到数据库的连接。该函数的第一个参数指定数据库文件的位置。这里有一个模糊的细节是设置这个位置的正确方法。虽然我们将从CGI赢博体育程序中打开url .db文件,并且CGI赢博体育程序与数据库文件位于同一目录中,但我们必须在这里使用“CGI /urls.db”而不是“urls.db”。这样做的原因与我们将要运行CGI赢博体育程序的方式有关:这些赢博体育程序将通过fork服务器启动,然后使用execute()来运行赢博体育程序。当您使用execve()启动赢博体育程序时,它将继承父赢博体育程序的赢博体育特征,包括当前工作目录。从服务器当前工作目录到数据库文件的路径是“cgi/urls.db”,所以这是我们在sqlite3_open()函数中指定的路径。

sqlite3_open()函数返回一个结果代码,该代码可以告诉用户与数据库的连接是否成功。如果此结果代码不是SQLITE_OK,我们只需让CGI赢博体育程序放弃并返回HTML 500内部服务器错误响应页面。

我们在这里要执行的数据库交互是一个SQL Insert语句。下面是执行此请求的代码:

//在数据库中存储URL char *err_msg = 0;char sql [128];sprintf(sql,"INSERT INTO Urls(URL) VALUES('%s');",buffer);rc = sqlite3_exec(db, sql, NULL, 0, &err_msg);

这里使用的缓冲区是一个字符数组,用于存储用户希望存储在数据库中的URL。我们使用sqlite3_exec()来执行SQL插入语句。

当我们执行这个插入语句时,SQLite将为URL添加一个新行到URL表中。该行将有一个自动生成的整数id。要了解这个id是什么,我们可以使用下面的代码:

//获取插入表项的id号sqlite3_int64 id = sqlite3_last_insert_rowid(db);

最后,要为用户生成缩短的URL,我们必须使用base64库的encode()函数:

//对id号进行编码;Encode ((unsigned int) id,code);

当我们完成与数据库的工作时,我们关闭与数据库的连接:

sqlite3_close (db);

解码CGI赢博体育程序

当服务器赢博体育程序接收到采用表单的GET请求时

http:// <服务器地址> / s / XXXXXX

它将首先将其重写为CGI GET请求URL的标准形式

http:// <服务器地址> / cgi /解码?代码= XXXXXX

然后,服务器将复制查询参数

代码= XXXXXX

到QUERY_STRING环境变量中,然后调用decode CGI赢博体育程序。

下面是解码赢博体育程序的代码:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sqlite3.h> #include <base64/base64.h> void errorResponse(){//发送一个错误响应printf("HTTP/1.1 500 Internal Server error \r\n");printf("连接:关闭\ r \ n ");printf("内容长度:21 \ r \ n ");printf("内容类型:文本/平原\ r \ n \ r \ n”);printf(“出错了”);。fflush (stdout);} char URL[256];int callback(void *NotUsed, int argc, char **argv, char **colNames) {if(strcmp(colNames[0],"URL") == 0) strncpy(URL,argv[0],255);else URL[0] = '\0';返回0;} int main() {int status = 0;sqlite3 *数据库;Int rc = sqlite3_open("cgi/urls.db", &db);char * codeStr;if ((rc == SQLITE_OK) && ((codeStr = getenv("QUERY_STRING"))) != NULL) && (strncmp(codeStr,"code=",5)==0)) {unsigned int code= decode(codeStr+5);Char *err_msg = 0;char sql [128];sprintf(sql,"SELECT URL from URL WHERE id = %u;",代码);Rc = sqlite3_exec(db, sql, callback, 0, &err_msg);如果(rc ! = SQLITE_OK | | URL [0] = = ' \ 0 ') {errorResponse ();if(rc != SQLITE_OK) sqlite3_free(err_msg);状态= 1;} else {printf("HTTP/1.1 301永久移动\n");printf("位置:");printf (" % s \ r \ n \ r \ n”,URL);。fflush (stdout);}} else {errorResponse();状态= 1;} sqlite3_close (db);返回状态;}

打开到数据库的连接并读取QUERY_STRING环境变量之后,赢博体育程序准备一个SQL选择查询来从数据库中获取URL。像往常一样,我们将使用SQLite sqlite3_exec()函数来运行该查询。这一次的不同之处在于,我们正在执行SQL Select,它将从数据库返回一行或多行数据。为了读取这些行返回的日期,我们必须提供一个指向回调函数的指针,SQLite库将为从数据库返回的每一行调用该回调函数。

下面是我们将要使用的回调函数:

int callback(void *NotUsed, int argc, char **argv, char **colNames) {if(argc > 0 && strcmp(colNames[0],"URL") == 0) strncpy(URL,argv[0],255);else URL[0] = '\0';返回0;}

SQLite库强制我们为这个回调函数使用一个非常特定的结构。argv形参是一个指向字符串数组的指针:该数组包含我们正在处理的行数据。colNames参数是一个指针,指向一个字符串数组,其中包含argv中存储的每个数据值的列名。参数argc告诉我们argv和colNames数组中有多少个字符串。

在本例中,我们期望从包含单列的数据库中返回一行。回调函数的代码确认返回的唯一列的名称是“URL”,然后将argv[0]中的数据项复制到一个全局数组URL中,该数组将存储返回的URL。最后,期望回调函数返回一个int,表示读取是否成功。

有了回调函数,我们可以运行查询来获取用户想要的行:

unsigned int code = decode(codeStr+5);Char *err_msg = 0;char sql [128];sprintf(sql,"SELECT URL from URL WHERE id = %u;",代码);Rc = sqlite3_exec(db, sql, callback, 0, &err_msg);

sqlite3_exec()的第三个参数是指定要使用的回调函数的地方。