HTTP服务器项目1:Linux系统编程入门

HTTP服务器项目1:Linux系统编程入门

1:Linux开发环境的搭建:

完成这个项目用到的开发环境为:

VMware
ubuntu18.04
VSCode
xshell/Mobaxterm

如果需要软件和镜像,可以在文章末尾进行提取。

2:GCC编译工具:

01 / 什么是GCC:
◼ GCC 原名为 GNU C语言编译器(GNU C Compiler) ◼ GCC(GNU Compiler Collection,GNU编译器套件)是由 GNU 开发的编程语言
译器。GNU 编译器套件包括 C、C++、Objective-C、Java、Ada 和 Go 语言前
端,也包括了这些语言的库(如 libstdc++,libgcj等)
◼ GCC 不仅支持 C 的许多“方言”,也可以区别不同的 C 语言标准;可以使用命令行
选项来控制编译器在翻译源代码时应该遵循哪个 C 标准。例如,当使用命令行参数
-std=c99 启动 GCC 时,编译器支持 C99 标准。
◼ 安装命令 sudo apt install gcc g++ (版本 > 4.8.5) ◼ 查看版本 gcc/g++ -v/–version

02 / 编程语言的发展:
在这里插入图片描述
03 / GCC工作流程:
在这里插入图片描述
04 / gcc 和 g++ 的区别:

◼ gcc 和 g++都是GNU(组织)的一个编译器。
◼ 误区一:gcc 只能编译 c 代码,g++ 只能编译 c++ 代码。两者都可以,请注意:
 后缀为 .c 的,gcc 把它当作是 C 程序,而 g++ 当作是 c++ 程序
 后缀为 .cpp 的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
 编译阶段,g++ 会调用 gcc,对于 C++ 代码,两者是等价的,但是因为 gcc
命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接,为了统
一起见,干脆编译/链接统统用 g++ 了,这就给人一种错觉,好像 cpp 程序只
能用 g++ 似的
◼ 误区二:gcc 不会定义 __cplusplus 宏,而 g++ 会  实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释
 如上所述,如果后缀为 .c,并且采用 gcc 编译器,则该宏就是未定义的,否则,
就是已定义
◼ 误区三:编译只能用 gcc,链接只能用 g++
 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用
gcc/g++,而链接可以用 g++ 或者 gcc -lstdc++。  gcc 命令不能自动和C++程序使用的库联接,所以通常使用 g++ 来完成联接。
但在编译阶段,g++ 会自动调用 gcc,二者等价

05 / GCC常用参数选项:
在这里插入图片描述
在这里插入图片描述
3:静态库的制作与使用

01 / 什么是库:
◼ 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用
者一些可以直接拿来用的变量、函数或类。
◼ 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
◼ 库文件有两种,静态库和动态库(共享库),区别是:静态库在程序的链接阶段被复制
到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加
载到内存中供程序调用。
◼ 库的好处:1.代码保密 2.方便部署和分发

02 / 静态库的制作:
在这里插入图片描述
03/静态库的使用:

ar rcs libxxx.a xxx.o xxx.o //获得静态库文件
将静态库保存在工程当中的lib/文件夹下
进行gcc编译
//gcc main.c -o app
出现如下错误:
//main.c:2:18: fatal error: head.h: 没有那个文件或目录
compilation terminated.
//原因:在当前路径下进行编译,没有加载
采用:gcc main.c -o app -I ./include/
之后又出现如下错误:
/tmp/ccLLpMtx.o:在函数‘main’中:
main.c:(.text+0x38):对‘add’未定义的引用
main.c:(.text+0x58):对‘subtract’未定义的引用
main.c:(.text+0x78):对‘multiply’未定义的引用
main.c:(.text+0x98):对‘divide’未定义的引用
collect2: error: ld returned 1 exit status
//原因:没有加载进静态库
采用:gcc main.c -o app -I ./include/ -l calc -L ./lib/
链接完成;生成可执行程序
./app

4:动态库的制作和使用:

01 / 动态库的制作:
在这里插入图片描述

02 / 工作原理:

◼ 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中
◼ 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中
◼ 程序启动之后,动态库会被动态加载到内存中,通过 ldd (list dynamic
dependencies)命令检查动态库依赖关系
◼ 如何定位共享库文件呢?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路
径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是
由ld-linux.so来完成的,它先后搜索elf文件的 DT_RPATH段 ——> 环境变量
LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib
目录找到库文件后将其载入内存。

03 / 动态库的使用:

//得到动态库:
gcc -c –fpic/-fPIC a.c b.c
gcc -shared a.o b.o -o libcalc.so
将静态库保存在工程当中的lib/文件夹下
//编译工程当中的main.c
gcc main.c -o main -I ./include/  -l calc -L ./lib/
生成可执行程序
./main  //进行执行
显示如下错误:
//./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
//原因:没有加载进动态库
ldd main  //发现无依赖的链接文件
env   //查看环境变量路径
配置环境变量:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/dazai/linux/lession06/library/lib   
echo $LD_LIBRARY_PATH  //查看配置结果
ldd main    //检查动态库依赖关系
//linux-vdso.so.1 =>  (0x00007ffdc5c34000)
        libcalc.so => /home/dazai/linux/lession06/library/lib/libcalc.so (0x00007f6e24fba000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6e24bf0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f6e251bc000)
配置成功;运行可执行程序:
./main

补充:关于动态库加载失败的问题,如果我们重新打开一个终端,又同样会出现如上的错误。
我们需要在home目录下进行一些配置:

vim .bashrc
在最下面插入:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/dazai/linux/lession06/library/lib
:wq   //保存并退出
之后通过:
. .bashrc   
使刚才的配置生效

4:静态库和动态库的对比

01 / 程序编译成可执行程序的过程:
在这里插入图片描述
02 / 静态库制作过程:
在这里插入图片描述
03 / 动态库制作过程:
在这里插入图片描述
04 / 静态库的优缺点:
在这里插入图片描述
05 / 动态库的优缺点:
在这里插入图片描述

5.Makefile程序管理工具:

01 / 什么是 Makefile

◼ 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,
Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile 文件就像一个 Shell 脚本一样,也可以执行操作系统的命令。

◼ Makefile 带来的好处就是“自动化编译” ,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。
make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,
比如 Delphi 的 make,Visual C++ 的 nmake,Linux 下 GNU 的 make。

02 / Makefile 文件命名和规则

◼ 文件命名
makefile 或者 Makefile

◼ Makefile 规则
 一个 Makefile 文件中可以有一个或者多个规则

目标 …: 依赖 …
命令(Shell 命令)

⚫ 目标:最终要生成的文件(伪目标除外)
⚫ 依赖:生成目标所需要的文件或是目标
⚫ 命令:通过执行命令对依赖操作生成目标(命令前必须 Tab 缩进)
Makefile 中的其它规则一般都是为第一条规则服务的

make指令的安装:

sudo apt install make

03 / 工作原理
◼ 命令在执行之前,需要先检查规则中的依赖是否存在
 如果存在,执行命令
 如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,
如果找到了,则执行该规则中的命令

◼ 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
 如果依赖的时间比目标的时间晚,需要重新生成目标
 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

04 / 变量
◼ 自定义变量

变量名=变量值 var=hello
◼ 预定义变量

AR : 归档维护程序的名称,默认值为 ar
CC : C 编译器的名称,默认值为 cc
CXX : C++ 编译器的名称,默认值为 g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件

◼ 获取变量的值
$(变量名)

在这里插入图片描述
05 / 模式匹配

在这里插入图片描述
06 / 函数

◼ $(wildcard PATTERN…)
 功能:获取指定目录下指定类型的文件列表
 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多
个目录,一般使用空格间隔
 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
 示例:

$(wildcard *.c ./sub/*.c)
返回值格式: a.c b.c c.c d.c e.c f.c

◼ $(patsubst ,, )
 功能:查找 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合
模式,如果匹配的话,则以替换。
 可以包括通配符 %,表示任意长度的字串。如果
中也包含 %,那么,中的这个 %将是中的那个%
所代表的字串。(可以用 \来转义,以 \%来表示真实含义的 %字符)  返回:函数返回被替换过后的字符串
 示例:

$(patsubst %.c, %.o, x.c bar.c)
返回值格式: x.o bar.o

在这里插入图片描述

make
make clean

6.GDB调试

01 / 什么是 GDB

◼ GDB 是由 GNU 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环
境,GDB 是 Linux 和许多类 Unix 系统中的标准开发环境。
◼ 一般来说,GDB 主要帮助你完成下面四个方面的功能:

  1. 启动程序,可以按照自定义的要求随心所欲的运行程序
  2. 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
  3. 当程序被停住时,可以检查此时程序中所发生的事
  4. 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG

在这里插入图片描述
02 / 准备工作

◼ 通常,在为调试而编译时,我们会()关掉编译器的优化选项(-O), 并打开调
试选项(-g)。另外,-Wall在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的 BUG。

◼ gcc -g -Wall program.c -o program

-g 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。

03 / GDB 命令 – 启动、退出、查看代码

◼ 启动和退出

gdb 可执行程序
quit

◼ 给程序设置参数/获取设置参数

set args 10 20
show args

◼ GDB 使用帮助

help

◼ 查看当前文件代码

list/l (从默认位置显示)
list/l 行号 (从指定的行显示)
list/l 函数名(从指定的函数显示)

◼ 查看非当前文件代码

list/l 文件名:行号
list/l 文件名:函数名

◼ 设置显示的行数

show list/listsize
set list/listsize 行数

04 / GDB 命令 – 断点操作

◼ 设置断点

b/break 行号
b/break 函数名
b/break 文件名:行号
b/break 文件名:函数

◼ 查看断点

i/info b/break

◼ 删除断点

d/del/delete 断点编号

◼ 设置断点无效

dis/disable 断点编号

◼ 设置断点生效

ena/enable 断点编号

◼ 设置条件断点(一般用在循环的位置)

b/break 10 if i==5

05 / GDB 命令 – 调试命令

◼ 运行GDB程序

start(程序停在第一行)
run(遇到断点才停)

◼ 继续运行,到下一个断点停

c/continue

◼ 向下执行一行代码(不会进入函数体)

n/next

◼ 变量操作

p/print 变量名(打印变量值)
ptype 变量名(打印变量类型)

◼ 向下单步调试(遇到函数进入函数体)

s/step
finish(跳出函数体)

◼ 自动变量操作

display 变量名(自动打印指定变量的值)
i/info display
undisplay 编号

◼ 其它操作

set var 变量名=变量值 (循环中用的较多)
until (跳出循环)

7.文件IO:

01 / 标准 C 库 IO 函数

在这里插入图片描述
02 / 标准 C 库 IO 和 Linux 系统 IO 的关系

在这里插入图片描述
03 / 虚拟地址空间

在这里插入图片描述
04 / 文件描述符
在这里插入图片描述
05 / Linux 系统 IO 函数

◼ int open(const char *pathname, int flags);
◼ int open(const char *pathname, int flags, mode_t mode);
◼ int close(int fd);
◼ ssize_t read(int fd, void *buf, size_t count);
◼ ssize_t write(int fd, const void *buf, size_t count);
◼ off_t lseek(int fd, off_t offset, int whence);
◼ int stat(const char *pathname, struct stat *statbuf);
◼ int lstat(const char *pathname, struct stat *statbuf);

06 / stat 结构体

struct stat {
    
    
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // 节点
mode_t st_mode; // 文件的类型和存取的权限
nlink_t st_nlink; // 连到该文件的硬连接数目
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // 设备文件的设备编号
off_t st_size; // 文件字节数(文件大小)
blksize_t st_blksize; // 块大小
blkcnt_t st_blocks; // 块数
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间
time_t st_ctime; // 最后一次改变时间(指属性)
};

07 / st_mode 变量
在这里插入图片描述
测试代码:实现 ls -l 命令操作:


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>

// 模拟实现 ls -l 指令
// -rw-rw-r-- 1 nowcoder nowcoder 12 12月  3 15:48 a.txt
int main(int argc, char * argv[]) {
    
    

    // 判断输入的参数是否正确
    if(argc < 2) {
    
    
        printf("%s filename\n", argv[0]);
        return -1;
    }

    // 通过stat函数获取用户传入的文件的信息
    struct stat st;
    int ret = stat(argv[1], &st);
    if(ret == -1) {
    
    
        perror("stat");
        return -1;
    }

    // 获取文件类型和文件权限
    char perms[11] = {
    
    0};   // 用于保存文件类型和文件权限的字符串

    switch(st.st_mode & S_IFMT) {
    
    
        case S_IFLNK:
            perms[0] = 'l';
            break;
        case S_IFDIR:
            perms[0] = 'd';
            break;
        case S_IFREG:
            perms[0] = '-';
            break; 
        case S_IFBLK:
            perms[0] = 'b';
            break; 
        case S_IFCHR:
            perms[0] = 'c';
            break; 
        case S_IFSOCK:
            perms[0] = 's';
            break;
        case S_IFIFO:
            perms[0] = 'p';
            break;
        default:
            perms[0] = '?';
            break;
    }

    // 判断文件的访问权限

    // 文件所有者
    perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
    perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
    perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';

    // 文件所在组
    perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
    perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
    perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';

    // 其他人
    perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
    perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
    perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';

    // 硬连接数
    int linkNum = st.st_nlink;

    // 文件所有者
    char * fileUser = getpwuid(st.st_uid)->pw_name;

    // 文件所在组
    char * fileGrp = getgrgid(st.st_gid)->gr_name;

    // 文件大小
    long int fileSize = st.st_size;

    // 获取修改的时间
    char * time = ctime(&st.st_mtime);

    char mtime[512] = {
    
    0};
    strncpy(mtime, time, strlen(time) - 1);

    char buf[1024];
    sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);

    printf("%s\n", buf);

    return 0;
}

08 / 文件属性操作函数

◼ int access(const char *pathname, int mode);
◼ int chmod(const char *filename, int mode);
◼ int chown(const char *path, uid_t owner, gid_t group);
◼ int truncate(const char *path, off_t length);

09 / 目录操作函数

◼ int rename(const char *oldpath, const char *newpath);
◼ int chdir(const char *path);
◼ char *getcwd(char *buf, size_t size);
◼ int mkdir(const char *pathname, mode_t mode);
◼ int rmdir(const char *pathname);

10 / 目录遍历函数

◼ DIR *opendir(const char *name);
◼ struct dirent *readdir(DIR *dirp);
◼ int closedir(DIR *dirp);

11 / dirent 结构体和 d_type

struct dirent
{
    
    
// 此目录进入点的inode
ino_t d_ino; 
// 目录文件开头至此目录进入点的位移
off_t d_off; 
// d_name 的长度, 不包含NULL字符
unsigned short int d_reclen; 
// d_name 所指的文件类型
unsigned char d_type; 
// 文件名
char d_name[256];
};

测试代码:读取某个目录下普通文件的个数:(使用到了递归)

/*
    // 打开一个目录
    #include <sys/types.h>
    #include <dirent.h>
    DIR *opendir(const char *name);
        参数:
            - name: 需要打开的目录的名称
        返回值:
            DIR * 类型,理解为目录流
            错误返回NULL


    // 读取目录中的数据
    #include <dirent.h>
    struct dirent *readdir(DIR *dirp);
        - 参数:dirp是opendir返回的结果
        - 返回值:
            struct dirent,代表读取到的文件的信息
            读取到了末尾或者失败了,返回NULL

    // 关闭目录
    #include <sys/types.h>
    #include <dirent.h>
    int closedir(DIR *dirp);

*/
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int getFileNum(const char * path);

// 读取某个目录下所有的普通文件的个数
int main(int argc, char * argv[]) {
    
    

    if(argc < 2) {
    
    
        printf("%s path\n", argv[0]);
        return -1;
    }

    int num = getFileNum(argv[1]);

    printf("普通文件的个数为:%d\n", num);

    return 0;
}

// 用于获取目录下所有普通文件的个数
int getFileNum(const char * path) {
    
    

    // 1.打开目录
    DIR * dir = opendir(path);

    if(dir == NULL) {
    
    
        perror("opendir");
        exit(0);
    }

    struct dirent *ptr;

    // 记录普通文件的个数
    int total = 0;

    while((ptr = readdir(dir)) != NULL) {
    
    

        // 获取名称
        char * dname = ptr->d_name;

        // 忽略掉. 和..
        if(strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) {
    
    
            continue;
        }

        // 判断是否是普通文件还是目录
        if(ptr->d_type == DT_DIR) {
    
    
            // 目录,需要继续读取这个目录
            char newpath[256];
            sprintf(newpath, "%s/%s", path, dname);
            total += getFileNum(newpath);
        }

        if(ptr->d_type == DT_REG) {
    
    
            // 普通文件
            total++;
        }


    }

    // 关闭目录
    closedir(dir);

    return total;
}

12 / dup、dup2 函数

◼ int dup(int oldfd);
复制文件描述符

◼ int dup2(int oldfd, int newfd);
重定向文件描述符

13 / fcntl 函数

◼ int fcntl(int fd, int cmd, … /* arg */ );
复制文件描述符
设置/获取文件的状态标志

测试代码:

/*

    #include <unistd.h>
    #include <fcntl.h>

    int fcntl(int fd, int cmd, ...);
    参数:
        fd : 表示需要操作的文件描述符
        cmd: 表示对文件描述符进行如何操作
            - F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
                int ret = fcntl(fd, F_DUPFD);

            - F_GETFL : 获取指定的文件描述符文件状态flag
              获取的flag和我们通过open函数传递的flag是一个东西。

            - F_SETFL : 设置文件描述符文件状态flag
              必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
              可选性:O_APPEND, O)NONBLOCK
                O_APPEND 表示追加数据
                NONBLOK 设置成非阻塞
        
        阻塞和非阻塞:描述的是函数调用的行为。
*/

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main() {
    
    

    // 1.复制文件描述符
    // int fd = open("1.txt", O_RDONLY);
    // int ret = fcntl(fd, F_DUPFD);

    // 2.修改或者获取文件状态flag
    int fd = open("1.txt", O_RDWR);
    if(fd == -1) {
    
    
        perror("open");
        return -1;
    }

    // 获取文件描述符状态flag
    int flag = fcntl(fd, F_GETFL);
    if(flag == -1) {
    
    
        perror("fcntl");
        return -1;
    }
    flag |= O_APPEND;   // flag = flag | O_APPEND

    // 修改文件描述符状态的flag,给flag加入O_APPEND这个标记
    int ret = fcntl(fd, F_SETFL, flag);
    if(ret == -1) {
    
    
        perror("fcntl");
        return -1;
    }

    char * str = "nihao";
    write(fd, str, strlen(str));

    close(fd);

    return 0;
}

补充
完成该项目所需要软件:VSCode / VMware / xshell / Ubuntu18.04光驱
链接:https://pan.baidu.com/s/11xojdNhim4fCK7VKyoJr3A
提取码:xmjx

猜你喜欢

转载自blog.csdn.net/weixin_40734514/article/details/114801251