关于socket:c语言实现一个简单的web服务器借助http协议

这个程序是看到的一本书上socket编程章节的课后题,题目内容很多,具体可见链接:
https://www.bookstack.cn/read…
实现一个简略的Web服务器myhttpd。服务器程序启动时要读取配置文件/etc/myhttpd.conf,其
中须要指定服务器监听的端口号和服务目录,我设置的是如下:
Port=8000
Directory=/var/www
在Directory即服务器的/var/www放入你想要加载到client(此处是浏览器)的文件,如图片或者是html文件。能够看到这是我的ecs外面放的要加载的文件。

多过程传输的server的代码如下所示,留神是在linux环境下编程和执行!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>

#define RECV_PORT    8000
#define QLEN        10
#define MAX_SIZE    1024

char browser_com_buf[256];
    
int mylisten (void)
{
    int listenfd;
    int err;
    socklen_t addrlen;
    struct sockaddr_in server_addr;
    char *address = "101.132.101.121";
    
    listenfd = socket ( AF_INET, SOCK_STREAM, 0 );   /*creat a new socket*/
    if ( listenfd < 0 )
    {
        printf ( "listen error\n" );
        return -1;
    }
    memset ( &server_addr, 0, sizeof ( server_addr ) );    
    server_addr.sin_family = AF_INET;    /*IPV4*/
    server_addr.sin_port = htons ( RECV_PORT );
    server_addr.sin_addr.s_addr = htonl ( INADDR_ANY );
    
    addrlen = sizeof ( server_addr );
    err = bind ( listenfd, ( struct sockaddr * )&server_addr, addrlen );
    if ( err < 0 )
    {
        printf ( "bind error\n" );
        return -2;
    }
    err = listen ( listenfd, QLEN );
    if ( err < 0 )
    {
        printf ( "listen error" );
        return -3;
    }
    return listenfd;
}

int myaccept ( int listenfd )
{
    int clientfd;
    int err;
    socklen_t addrlen;
    struct sockaddr_in client_addr;
    
    addrlen = sizeof ( client_addr );
    clientfd = accept ( listenfd, ( struct sockaddr * )&client_addr, &addrlen );
    if ( clientfd < 0 )
    {
        printf ( "accept error\n" );
        return -1;
    }
    return clientfd;
}

void browser_com_analysis ( char *com, char *buf )
{
    int i = 0;
    unsigned int flag = 0;
    char *locate;
    
    locate = strchr ( com, '/' );
    //顺次检索com字符串中的每一个字符,直到遇见字符 /,返回第一次呈现字符/的地位
    if ( locate == NULL )
    {
        printf ( "not find\n" );
        exit ( 1 );
    }
    else
    {
        //把命令逐字符存到buf中
        while ( *locate != ' ' )
        {
            buf[i++] = *locate;
            locate++;
        }
        buf[i] = '\0';
    }
    //printf ( "%s\n", buf );
}

//判断文件的权限和状态,是纯txt/html还是图片,还是CGI程序(可执行文件)
int state_estimat ( char *buf )
{
    int len;
    unsigned int flag = 0;
    int i = 0, j = 0;
    char buf1[256];
    char *image_style = ".jpg";
    char file_name[256] = { "/var/www" };
    struct stat statbuf;
    
    len = strlen ( buf );
    memset ( buf1, '\0', sizeof ( buf1 ) );
    while ( i < len )
    {
        //将文件格式后缀的内容存到buf[1],如test.jpg将.jpg存到buf1中
        if ( buf[i] == '.' )
        {
            flag = 1;
        }
        if ( flag == 1 )
        {
            if ( buf[i] == ' ' || buf[i] == '\n' )
            {
                break;
            }
            else
            {
                buf1[j++] = buf[i];
            }
        }
        i++;
    }
    //printf ( "%s%d\n", buf1, len );
    if ( len == 0 )
    {
        printf ( "http have not send comand\n" );
        exit ( 1 );
    }
    //GET 之后只有一个命令/,所以len == 1,在本例中/实际上是/var/www
    //Web服务器应该把该目录下的索引页(默认是index.html)发给浏览器.
    //也就是把/var/www/index.html发给浏览器
    else if ( len == 1 )
    {
        return 1;
    }
    else if ( len > 1 )
    {
        //将参数 src 字符串复制到参数 dest 所指的字符串尾部;dest 最初的完结字符 NULL 会被笼罩掉.
        //并在连贯后的字符串的尾部再减少一个 NULL。
        strcat ( file_name, buf );
        stat ( file_name, &statbuf );
        //stat()用来将参数file_name 所指的文件状态, 复制到参数statbuf所指的构造中
        if ( statbuf.st_mode & S_IXOTH )   //其余用户具备可执行权限
        {
            //如果具备可执行权限,则执行该文件,将后果发送到浏览器上
            return 4;
        }
        else
        {
            //不具备可执行权限则判断图片还是文件
            if ( strcmp ( buf1, image_style ) == 0 )
            {
                return 2;    //浏览器申请的是图片
            }
            return 3;    //浏览器申请的是文件
        }
    }
}

int find_file ( char *buf, int clientfd )
{
    int state;
    char my_file_buf[65536 * 2];
    int n;
    char fault[] = { "HTTP/1.1 404 Not Found\nContent-Type: text/html\n<html><body>request file not found</body></html>\n\n" };
    char head[] = { "HTTP/1.1 200 OK\nContent-Type: text/html\n\n" };
    char head2[] = { "HTTP/1.1 200 OK\nContent-Type: image/jpg\n\n" };
    char head3[] = { "HTTP/1.1 200 OK\n\n" };
    char file_name[] = { "/var/www" };
    
    memset ( &my_file_buf, '\0', MAX_SIZE );
    state = state_estimat ( buf );
    //Web服务器应该把该目录下的索引页(默认是index.html)
    if ( state == 1 )  //server send file to browser
    {
        int filefd;
        
        filefd = open ( "/var/www/index.html", O_RDWR );//返回0示意胜利
        //参数 pathname 指向欲关上的文件门路/var/www/index.html.
        if ( filefd < 0 )
        {
            printf ( "find file failed\n" );
            write ( clientfd, fault, strlen ( fault ) );
            close ( clientfd );
        }
        else
        {
            n = read ( filefd, my_file_buf, MAX_SIZE );//胜利返回读到的字节数
            //read()会把参数fd 所指的文件传送count个字节到buf 指针所指的内存中
            if ( n < 0 )
            {
                printf ( "read the root file failed\n" );
                exit ( 1 );
            }
            strcat ( head, my_file_buf );//将从文件获取到的内容放到head头内容 的上面
            printf ( "%s", head );
            write ( clientfd, head, strlen ( head ) );//胜利返回理论写入的字节数
            //把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内
            close ( clientfd );  
        }
    }
    else if ( state == 2 )   
    {
        printf ( "picture\n" );
        int picture_fd;    
        int n;
        
        write ( clientfd, head2, strlen ( head2 ) );
        memset ( &my_file_buf, '\0', sizeof ( my_file_buf ) );
        strcat ( file_name, buf );
        picture_fd = open ( file_name, O_RDONLY );   //只读关上图片文件
        if ( picture_fd  < 0 )
        {
            printf ( "open picture failed\n" );
            exit ( 1 );
        }
        n = read ( picture_fd, my_file_buf, sizeof ( my_file_buf ) );
        //将fd所指的文件传送count个字节到my_file_buf,此处文件是图片,所以传的是二进制码
        if ( n < 0 )
        {
            printf ( "read picture data failed\n" );
            exit ( 1 );
        }
        write ( clientfd, my_file_buf, sizeof ( my_file_buf ) );
        close ( clientfd );
    }
    else if ( state == 3 )
    {
        //3是指一般不可执行文件
        printf ( "file\n" );
        int file_fd;
        int n;
        
        write ( clientfd, head, strlen ( head ) );//head是“text/html”
        memset ( &my_file_buf, '\0', sizeof ( my_file_buf ) );
        strcat ( file_name, buf );//buf是指浏览器申请的文件名放在file_name /var/www之后
        
        file_fd = open ( file_name, O_RDONLY );
        if ( file_fd < 0 )
        {
            printf ( "open unCGI-file failed\n" );
            exit ( 1 );
        }
        n = read ( file_fd, my_file_buf, sizeof ( my_file_buf ) );
        if ( n < 0 )
        {
            printf ( "read unCGI-file failed \n" );
            exit ( 1 );
        }
        write ( clientfd, my_file_buf, sizeof ( my_file_buf ) );
    }
    else if ( state == 4 )
    {
        //是可执行文件
        printf ( "executable file\n" );
        pid_t pid;
    
        pid = fork ();
        if ( pid < 0 )
        {
            printf ( "creat child failed\n" );
            exit ( 1 );
        }
        else if ( pid > 0 )    //parent
        {
            int stateval;
            waitpid ( pid, &stateval, 0 );
            //waitpid()会临时进行目前过程的执行, wait有信号来到或子过程完结而后彻底清除该子过程
            //如果在调用wait()时子过程曾经完结, 则wait()会立刻返回子过程完结状态值. 
            //子过程的完结状态值会由参数status返回
            //wait期待第一个终止的子过程,而waitpid能够通过pid参数指定期待哪一个子过程
            close ( clientfd );
        }
        else    //child用来执行该可执行文件并返回后果到浏览器输入
        {
            int err;
            char *argv[1];//char* argv[1]指针数组,只有一个指针元素
            
            strcat ( file_name, buf );
            argv[1] = file_name;
            dup2 ( STDOUT_FILENO, clientfd );
            //用dup2重定向子过程的规范输入到客户端socket
            err = execv ( "/bin/bash", argv );//执行CGI 如/bin/bash test.sh
            //其规范输入会向dup2指定的那样定向到浏览器
            //不带字母p(示意path)的exec函数第一个参数必须是程序的相对路径或绝对路径
            if ( err < 0 )
            {
                printf ( "executable file failed\n" );
                exit ( 1 );
            }
            close ( clientfd );
        }
    }
}

int main ( int argc, char *argv[] )
{
    int listenfd, clientfd;
    int filefd, n;
    char recvbuf[MAX_SIZE];
    char data_buf[MAX_SIZE];
    
    listenfd = mylisten();
    if ( listenfd < 0 )
    {
        exit ( 0 );
    }
    
    printf ( "listening and listen fd is %d......\n", listenfd );
    while ( 1 )
    {
        clientfd = myaccept ( listenfd );
        printf ( "accept success......\n" );
        if ( clientfd < 0 )
        {
            exit ( 1 );
        }
        memset ( &recvbuf, '\0', MAX_SIZE );
        memset ( &data_buf, '\0', MAX_SIZE );
        n = read ( clientfd, recvbuf, MAX_SIZE );
        printf ( "read OK\n" );
        if ( n < 0 )
        {
            printf ( "read error\n" );
            exit ( 2 );
        }
        
        filefd = open ( "/tmp/read_html.txt", O_RDWR | O_CREAT | O_TRUNC );   //open o new file
        write ( filefd, recvbuf, n );  //save the http information to filefd
        browser_com_analysis ( recvbuf, data_buf );//将收到的命令解析出/+前面有用的保留在data_buf
        find_file ( data_buf, clientfd );//对浏览器的申请内容进行判断,是一般html还是picture或是CGI
    }
    close ( clientfd );    
    
    return 0;
}

让咱们来看一下成果:

1、首先在linux中运行该程序,此处我应用的vscode近程连贯linux,能够间接运行server,如果你是在centos默认的界面或者vim编辑,能够应用gcc http_sever.c -o http_sever生成可执行文件 和 ./http_sever来执行该文件,能够看到服务器开始listen监听是否有连贯申请:

2、关上浏览器,输出本人的服务器IP,例如我的是http://101.132.101.121 ,如果端口号不是80,我设置的是8000,则输出 http://101.132.101.121:8000,回车向服务器发出请求,即可看到你要加载到浏览器的数据!

3、回到服务器端,查看数据传输胜利的相干回执,此处我用的是vscode近程连贯代替vim编辑器和linux命令行,便于debug。第一张图是轻易写的测试用的index.html文件,并导入了一张图片。

能够看到html文件和图片均接管胜利,咱们在网页中也看到了,而可执行文件打不开,因为这个文件夹刚好是tomcat的部署文件夹,所以有一些部署文件在外面,客户端试着关上他们当然失败了!
所以将如果你像我一样曾经部署过tomcat的话,倡议将部署文件夹var/www/更换一下!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理