2023年3月

常见场景

你是在工作中遇到如下问题或者疑问:

  1. undefined reference to “function”。链接过程中出现未定义引用。
  2. .a和.so文件分别是什么?什么情况下使用?
  3. extern "C"有什么作用?
    等等...

编译过程

我们平时编译,如果没有加任何编译参数将默认执行预处理,编译,汇编,链接等步骤。

ELF文件格式

每一个cpp文件会生成一个.o文件。.o文件里面有什么信息?多个.o文件如何合并成一个可执行文件。可执行文件的文件里有都有什么信息?
看下下面的例子:

int global_init_var = 84;
int global_uninit_var;

void func1(int i) {
    printf("%d\n", i);
}

int main() {
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
    return 0;
}

为了探究.o文件内容,只编译不链接
gcc -c whats_in_elf.c -o whats_in_elf.o
ELF可以用objdump,readelf等工具查看内容。这里用readelf -S whats_in_elf.o查看section headers:

# daihaonan link_load $ readelf -S whats_in_elf.o
There are 11 section headers, starting at offset 0x114:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000051 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 000424 000028 08      9   1  4
  [ 3] .data             PROGBITS        00000000 000088 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000090 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 000090 000004 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 000094 00002d 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 0000c1 000000 00      0   0  1
  [ 8] .shstrtab         STRTAB          00000000 0000c1 000051 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 0002cc 0000f0 10     10  10  4
  [10] .strtab           STRTAB          00000000 0003bc 000065 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

可以看到.o文件由很多section组成,每个section都有size, file off等描述其在文件内位置的属性。元信息记录在File header中,其中有e_shoff字段指向Section Header Table,Section Header Table是个数组结构保存每个Section信息。
查看Header:

.o文件总体格式如下:

当然还有很多其它section,.text,.data,.rodata,.symtab,.rel.text段是最主要的段,分别保存代码信息,全局数据,全局只读数据,符号表,代码段重定位表。

.text Section

将代码反汇编
objdump -s -d whats_in_elf.o

# daihaonan link_load $ objdump -d whats_in_elf.o

whats_in_elf.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	89 7d fc             	mov    %edi,-0x4(%rbp)
   b:	8b 45 fc             	mov    -0x4(%rbp),%eax
   e:	89 c6                	mov    %eax,%esi
  10:	bf 00 00 00 00       	mov    $0x0,%edi
  15:	b8 00 00 00 00       	mov    $0x0,%eax
  1a:	e8 00 00 00 00       	callq  1f <func1+0x1f>
  1f:	c9                   	leaveq
  20:	c3                   	retq

0000000000000021 <main>:
  21:	55                   	push   %rbp
  22:	48 89 e5             	mov    %rsp,%rbp
  25:	48 83 ec 10          	sub    $0x10,%rsp
  29:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
  30:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 36 <main+0x15>
  36:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 3c <main+0x1b>
  3c:	8d 04 02             	lea    (%rdx,%rax,1),%eax
  3f:	03 45 f8             	add    -0x8(%rbp),%eax
  42:	03 45 fc             	add    -0x4(%rbp),%eax
  45:	89 c7                	mov    %eax,%edi
  47:	e8 00 00 00 00       	callq  4c <main+0x2b>
  4c:	b8 00 00 00 00       	mov    $0x0,%eax
  51:	c9                   	leaveq
  52:	c3                   	retq

可以看到func1和main两个函数的反汇编代码。
顺便可以了解下gcc函数调用约定。

规则如下:

  1. 执行call指令前,函数调用者将参数入栈,按照函数列表从右到左的顺序入栈。
  2. call指令会自动将当前eip入栈,ret指令将自动从栈中弹出该值到eip寄存器。
  3. 被调用函数负责:将ebp入栈,esp的值赋给ebp。所以反汇编一个函数会发现开头两个指令都是
    push %ebp, mov %esp,%ebp
    一个例子:

.data和.rodat Section

Contents of section .data:
 0000 54000000 55000000                    T...U...
Contents of section .rodata:
 0000 25640a00                             %d..

可以看到.data Section有8个字节,分别是0x54和0x55对应全局变量static_var和global_init_var。
.rodata Section只有4个字节保存%d\n三个字符。

从这里可以直观看到全局有初值的变量是会在ELF文件中分配空间的,而a,b这种栈上分配的变量不会ELF文件中分配空间,只会在运行到该函数的是在栈上动态分配。

.symtab Section

可以用
readelf -s whats_in_elf.o
查看符号表

# daihaonan link_load $ readelf -s whats_in_elf.o

Symbol table '.symtab' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS whats_in_elf.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1600
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1601
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
    13: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 func1
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    15: 0000000000000021    50 FUNC    GLOBAL DEFAULT    1 main

从上面可以得到如下信息:

  1. 该.o文件中有static_var.1600,static_var2.1601,global_init_var,global_uninit_var,func1,printf,main等符号
  2. 每个符号在.o文件中的位置,比如func1,Ndx是1,对应.text Section,Value为0,Size为33,说明func1从.text Section起始字节开始,占了33个字节。
  3. printf这个符号在.o文件中并没有定义,所以它的Ndx是UND


g++ whats_in_elf.c -o whats_in_elf2.o
重新编译,会发现

# daihaonan link_load $ readelf -s whats_in_elf2.o

Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS whats_in_elf.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 _ZZ4mainE10static_var
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 _ZZ4mainE11static_var2
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    13: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 _Z5func1i
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __gxx_personality_v0
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z6printfPKcz
    16: 0000000000000021    50 FUNC    GLOBAL DEFAULT    1 main

原来的func1变成了_Z5func1i,为了防止符号冲突,C++引入了符号修饰的概念。
所以在C++里如果希望动态库中某个函数能被正确加载,需要加上
extern "C"
方式符号被修饰,比如:

extern "C"
{
    ProcessorBase* create_processor(const std::string& processor_name)
    {
        ...
    }
}

加载该符号的地方才能正确找到create_processor这个符号。
(PFUNC_CREATE_PROCESSOR_CALL)dlsym(handle,"create_processor");

.rel.text Section

对于可重定位的ELF文件,必须包含重定位Section,一个ELF文件中可能有多个重定位Section,比如.text有需要重定位的地方,那么会有一个.rel.text表,详细见下文。

静态链接

为什么需要链接?

考虑如下程序:
a.c

extern int shared;

int main() {
    int a = 100;
    swap(&a, &shared);
}

b.c

int shared = 1;

void swap(int* a, int* b) {
    *a ^= *b ^= *a ^= *b;
}

分别将a.c和b.c进行编译,然后查看代码段反汇编。

# daihaonan link_load $ objdump -d a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
   f:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 15 <main+0x15>
  15:	8b 45 fc             	mov    -0x4(%rbp),%eax
  18:	89 d6                	mov    %edx,%esi
  1a:	89 c7                	mov    %eax,%edi
  1c:	b8 00 00 00 00       	mov    $0x0,%eax
  21:	e8 00 00 00 00       	callq  26 <main+0x26>
  26:	c9                   	leaveq
  27:	c3                   	retq

main中会引用全局变量shared,调用swap函数,但是shared和swap都不是定义在a.o中的,而是定义在b.o中。所以a.o中对shared的引用为0x0(%rip),%rip寄存器中保存的是当前执行指令的地址,对swap调用为e8 00 00 00 00,这是一条
近址相对位移调用指令
,e8是指令码,00 00 00 00是操作数,也就是被调用函数相对于
调用指令的下一条指令
的偏移量。这里因为不知道swap函数在哪,所以暂时用00 00 00 00来代替。

所以我们可以得出链接的一个主要作用是对一些全局变量,函数引用指令进行修正。

链接后达到什么效果?

将a.o和b.o链接在一起。ld a.o b.o -e main -o ab
然后再来看下ab中main的反汇编代码

# daihaonan link_load $ objdump -S ab

ab:     file format elf64-x86-64


Disassembly of section .text:

00000000004000e8 <main>:
  4000e8:	55                   	push   %rbp
  4000e9:	48 89 e5             	mov    %rsp,%rbp
  4000ec:	48 83 ec 10          	sub    $0x10,%rsp
  4000f0:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
  4000f7:	8b 15 bb 00 20 00    	mov    0x2000bb(%rip),%edx        # 6001b8 <shared>
  4000fd:	8b 45 fc             	mov    -0x4(%rbp),%eax
  400100:	89 d6                	mov    %edx,%esi
  400102:	89 c7                	mov    %eax,%edi
  400104:	b8 00 00 00 00       	mov    $0x0,%eax
  400109:	e8 02 00 00 00       	callq  400110 <swap>
  40010e:	c9                   	leaveq
  40010f:	c3                   	retq

链接后再来看main函数的反汇编代码。有三个地方变动了
mov 0x0(%rip),%edx
变成了
mov 0x2000bb(%rip),%edx

e8 00 00 00 00
变成了
e8 02 00 00 00
。最左侧的地址变成了全局的虚拟地址,这说明链接还会分配虚拟地址空间,链接结束,每个函数,每个全局变量在虚拟地址空间内的地址就确定了。
callq下一条指令地址为0x40010e再加上0x02,等于0x400110。所以swap函数代码起始地址应该是0x400110。用objdump -S ab来验证下。

0000000000400110 <swap>:
  400110:	55                   	push   %rbp
  400666666:	48 89 e5             	mov    %rsp,%rbp
  400114:	53                   	push   %rbx
  400115:	48 89 7d f0          	mov    %rdi,-0x10(%rbp)
  400119:	48 89 75 e8          	mov    %rsi,-0x18(%rbp)
  40011d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  400121:	8b 10                	mov    (%rax),%edx
  400123:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  400127:	8b 08                	mov    (%rax),%ecx
  400129:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  40012d:	8b 18                	mov    (%rax),%ebx
  40012f:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  400133:	8b 00                	mov    (%rax),%eax
  400135:	31 c3                	xor    %eax,%ebx
  400137:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  40013b:	89 18                	mov    %ebx,(%rax)
  40013d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  400141:	8b 00                	mov    (%rax),%eax
  400143:	31 c1                	xor    %eax,%ecx
  400145:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  400149:	89 08                	mov    %ecx,(%rax)
  40014b:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  40014f:	8b 00                	mov    (%rax),%eax
  400151:	31 c2                	xor    %eax,%edx
  400153:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  400157:	89 10                	mov    %edx,(%rax)
  400159:	5b                   	pop    %rbx
  40015a:	c9                   	leaveq
  40015b:	c3                   	retq

果然swap起始地址是0x400110。

a.o+b.o到ab的过程大致如下图:

第一步对a.o和b.o相同的Section进行并合。
第二步将ab映射到进行虚拟地址空间,并确定各符号在进行虚拟地址空间中的地址。
第三步修正各符号引用,使其指向符号最终的地址。

怎么链接?

链接一般分为两步:

  1. 空间和地址分配。扫码所有输入目标文件,搜集符号定义和引用,放到全局符号表,并对Section进行并合。
  2. 符号解析和重定位。

符号重定位依赖重定位表+符号表

# daihaonan link_load $ objdump -r a.o

a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000011 R_X86_64_PC32     shared-0x0000000000000004
0000000000000022 R_X86_64_PC32     swap-0x0000000000000004

重定位表中记录了哪些地方需要修正,这里可以看到.text的0x11偏移处引用了shared变量,所以需要修正,.text的0x22偏移处引用了swap函数,也需要修正,
而.symtab Section记录了符号所在的位置。

# daihaonan link_load $ readelf -s b.o

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
     9: 0000000000000000    76 FUNC    GLOBAL DEFAULT    1 swap

链接器有了这俩信息,可以轻而易举完成符号重定位。

动态链接

静态链接VS动态链接

动态链接优点:

  1. 方便发布。模块A依赖模块B,如果模块B实现发生了改变,在静态链接的情况下,模块A需要重新编译。
  2. 内存占用。模块A和模块B都依赖模块C中的某个函数func,在果静态链接的情况下,模块A/B同时运行时,func需要在内存中存在两份。

动态链接缺点:

  1. 执行效率不如静态链接高。

动态链接效果

静态共享库

如图假设A.so又依赖B.so中的a变量和foo函数,当调用foo的时候,动态链接器会将B.so加载到内存load_address处,foo在B.so内是固定的y字节偏移出。所以foo在进程内的虚拟地址就是load_address+y。然后动态链接器修改A.so中call foo指令出代码,将foo地址修改为load_address+y。至此动态链接完成。和静态链接的区别在于动态链接将地址重定位推迟到了运行时。

动态共享库

上面这种静态共享库有个问题,就是指令部分没法在多个进程之间共享,从而失去了节省内存的优点。
假设有两个进程,做的事情都是A.so中调用B.so中的foo函数和引用a变量。
进程1A.so被加载到a0虚拟地址,进程2中A.so被动态加载到a1虚拟地址,静态共享库的虚拟内存分布如下:

A.so中的代码会被重定位,并且重定位值不一样,进程1中a变量在虚拟地址load_address1+x处,而在进程2中a变量在虚拟地址load_address2+x处。所以A.so的代码在内存中需要保存多份。

如果我们把需要重定位的地方单独抽出来放到数据区,这样a变量被加载到哪个地址,代码部分都不需要变动,那么两个进程可以只在物理内存中加载一份代码。使用这种机制的共享库叫做动态共享库。
相同的动态共享库的虚拟内存分布如下:

这种模式下,代码中需要被重定位的地方被放到了GOT中,动态加载重定位的时候只需要修改GOT就可以了,代码部分不需要被修改。缺点也很明显就是多了一层索引。

这就是-fPIC链接选项的作用。该链接选项指定生成的动态库为动态共享库。

1. 什么是Application








1.0 什么是基础不可变设施

GitOps当中是这样定义的。应用都需要运行在多台机器上,它们被组织成不同的环境,例如开发环境、测试环境和生产环境等等。需要将相同的应用部署到不同的机器上。通常需要系统管理员确保所有的机器都处于相同的状态。接着所有的修改、补丁、升级需要在所有的机器中进行。随着时间的推移,很难再确保所有的机器处于相同的状态,同时越来越容易出错。这就是传统的可变架构中经常出现的问题。这时我们有了不可变架构,它将整个机器环境打包成一个单一的不可变单元,而不是传统方式仅仅打包应用。这个单元包含了之前所说的整个环境栈和应用所有的修改、补丁和升级,这就解决了前面的问题。 —— 摘自 InfoQ 的《关于不可变架构以及为什么需要不可变架构》作者 百占辉

1.1 Application核心组件


Synced:一致
OutOfSync:不一致


Healthy:健康
Degraded:降级
Missing:缺失,即在GitRepo中存在资源定义,但并未完成部署
image

2. ArgoCD Application的创建

ArgoCD可以基于WEB-UI的方式来进行应用的发布,也可以基于Configuration List的方式去部署应用。

2.1 查看ArgoCD支持的API-Resources

kubectl api-resources --api-group=argoproj.io
NAME              SHORTNAMES         APIVERSION             NAMESPACED   KIND
applications      app,apps           argoproj.io/v1alpha1   true         Application
applicationsets   appset,appsets     argoproj.io/v1alpha1   true         ApplicationSet
appprojects       appproj,appprojs   argoproj.io/v1alpha1   true         AppProject

2.2 查看ArgoCD的字段属性

explain可以分级查看字段属性

[root@c-k-m1-10 argocd]# kubectl explain application
KIND:     Application
VERSION:  argoproj.io/v1alpha1

DESCRIPTION:
     Application is a definition of Application resource.

FIELDS:
   apiVersion	<string>
     APIVersion defines the versioned schema of this representation of an
     object. Servers should convert recognized schemas to the latest internal
     value, and may reject unrecognized values. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

   kind	<string>
     Kind is a string value representing the REST resource this object
     represents. Servers may infer this from the endpoint the client submits
     requests to. Cannot be updated. In CamelCase. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

   metadata	<Object> -required-
     Standard object's metadata. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

   operation	<Object>
     Operation contains information about a requested or running operation

   spec	<Object> -required-
     ApplicationSpec represents desired application state. Contains link to
     repository with application definition and additional parameters link
     definition revision.

   status	<Object>
     ApplicationStatus contains status information for the application

2.3 准Git源

GitOps中定义以特定Repository(配置仓库)为应用程序部署和管理的唯一可信源,该Repository负责定义Application的期望状态。本次测试使用gitee作为唯一的可信源。支持更多的配置管理工具例如helm、kustomize、jsonnet等;本次使用kubernetes原生的配置清单包含如下一个namespace一个裸Pod以及一个Service。

kind: Namespace
apiVersion: v1
metadata:
  name: hello
  
apiVersion: v1
kind: Service
metadata:
  name: hello-svc
  namespace: hello
spec:
  type: NodePort
  selector:    
    app: hello
  ports:
  - name: http         # 端口名称
    protocol: TCP      # 协议类型,目前支持TCP、UDP、SCTP默认为TCP
    port: 80           # Service的端口号
    targetPort: 8080   # 后端目标进程的端口号
    nodePort:
	
apiVersion: v1
kind: Pod
metadata:
  name: hello
  namespace: hello
  labels:
     app: hello
spec:
  containers:
  - name: hello
    image: lihuahaitang/helloworld:v1
    imagePullPolicy: IfNotPresent

2.4 编辑资源配置清单;

[root@c-k-m1-10 argocd]# cat application-hello.yaml
apiVersion: argoproj.io/v1alpha1   # 定义的API版本,可通过API-Resources查看
kind: Application  # 定义的资源类型
metadata:
  name: hello  # 名称
  namespace: argocd   # argocd所在的名称空间
spec:
  project: default   # 指明所属的项目是default
  source:     # 配置仓库及相关的配置访问的方法
    repoURL: https://gitee.com/good-news/apps.git   # 资源配置清单的Git的仓库源地址
    targetRevision: HEAD                  # 期望基于哪个修订版本来部署 
    path: kubernetes    # Git仓库的子目录路径
  destination:       # 应用程序要部署到的目标位置
    server: https://kubernetes.default.svc     # 目标kubernetes集群的API-Server访问入口,这里为本地集群
    namespace: hello          # 目标应用要部署的名称空间
  syncPolicy:                 # 同步策略,如果不写默认就是Manual为手动同步
    automated: null                # 为自动同步策略

2.5 查看应用状态

这里的应用状态为未同步,因为我们未指定同步策略为自动。默认为手动同步;

[root@c-k-m1-10 argocd]# argocd app list
WARN[0000] Failed to invoke grpc call. Use flag --grpc-web in grpc calls. To avoid this warning message, use flag --grpc-web. 
NAME          CLUSTER                         NAMESPACE  PROJECT  STATUS  HEALTH  SYNCPOLICY  CONDITIONS  REPO                                  PATH        TARGET
argocd/hello  https://kubernetes.default.svc  hello      default                  <none>      <none>      https://gitee.com/good-news/apps.git  kubernetes  HEAD

2.6 手动执行同步策略

[root@c-k-m1-10 argocd]# argocd app sync hello
WARN[0000] Failed to invoke grpc call. Use flag --grpc-web in grpc calls. To avoid this warning message, use flag --grpc-web. 
TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS   HEALTH        HOOK  MESSAGE
2023-03-25T22:00:35+08:00            Service     default                 hello   Unknown  Healthy              
2023-03-25T22:00:37+08:00            Service     default                 hello   Unknown  Healthy              ignored (requires pruning)
2023-03-25T22:00:37+08:00          Namespace       hello                 hello   Running   Synced              namespace/hello created
2023-03-25T22:00:37+08:00            Service       hello             hello-svc   Running   Synced              service/hello-svc created
2023-03-25T22:00:37+08:00                Pod       hello                 hello   Running   Synced              pod/hello created
2023-03-25T22:00:37+08:00            Service     default                 hello  OutOfSync  Healthy                  ignored (requires pruning)
2023-03-25T22:00:37+08:00            Service       hello             hello-svc  OutOfSync  Healthy                  service/hello-svc created
2023-03-25T22:00:37+08:00                Pod       hello                 hello    Synced   Progressing              pod/hello created
2023-03-25T22:00:37+08:00          Namespace                             hello    Synced                            

Name:               argocd/hello
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          hello
URL:                https://argocd.k8s.local/applications/hello
Repo:               https://gitee.com/good-news/apps.git
Target:             HEAD
Path:               kubernetes
SyncWindow:         Sync Allowed
Sync Policy:        <none>
Sync Status:        OutOfSync from HEAD (c916463)
Health Status:      Healthy

Operation:          Sync
Sync Revision:      c916463463c2244ae78ba442a0de764b743a493b
Phase:              Succeeded
Start:              2023-03-25 22:00:34 +0800 CST
Finished:           2023-03-25 22:00:37 +0800 CST
Duration:           3s
Message:            successfully synced (all tasks run)

GROUP  KIND       NAMESPACE  NAME       STATUS     HEALTH   HOOK  MESSAGE
       Service    default    hello      OutOfSync  Healthy        ignored (requires pruning)
       Namespace  hello      hello      Running    Synced         namespace/hello created
       Service    hello      hello-svc  OutOfSync  Healthy        service/hello-svc created
       Pod        hello      hello      Synced     Healthy        pod/hello created

2.7 查看名称空间的Pod以及Service

[root@c-k-m1-10 argocd]# kubectl get po,svc -n hello 
NAME        READY   STATUS    RESTARTS   AGE
pod/hello   1/1     Running   0          5m22s

NAME                TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/hello-svc   NodePort   xx.xx.xx.xx   <none>        80:32618/TCP   5m22s

2.8 WEBUI查看应用状态

image

2.9 尝试访问应用

sh-3.2# curl -I http://xx.xx.xx.xx32618/
HTTP/1.1 200 OK
Date: Sat, 25 Mar 2023 14:07:57 GMT
Connection: keep-alive

1、用户及passwd文件

1)掌握/etc/passwd文件的功能:存储所有用户的相关信息,每一个用户占用一行记录,该文件也被称为用户信息数据库(Database)
2)/etc/passwd文件中每个字段的具体含义:
a)第1个字段(列):记录的是这个用户的名字(在创建用户时root用户起的)
b)第2个字段(列):如果是x,表示该用户登录Linux系统时必须使用密码;如果为空,表示该用户在登录时无须提供密码
c)第3个字段(列):记录的是这个用户的uid
d)第4个字段(列):记录的是这个用户所属群组的gid
e)第5个字段(列):记录的是有关这个用户的备注信息
f)第6个字段(列):记录的是这个用户的家目录的路径
g)第7个字段(列):记录的是这个用户登录后,第一个要执行的进程
2、shadow文件
1)掌握/etc/shadow文件的功能:存储所有用户的密码,每一个用户占用一行记录,该文件实际上就是存放用户密码的数据库(Database)
2)掌握/etc/shadow文件中每个字段的具体含义:
a)第1个字段(列):记录的是用户名
b)第2个字段(列):记录的是密码,这个密码是经过md5加密算法加密过的密码
3、群组及group文件
1)掌握/etc/group文件的功能:存放了Linux系统中所有群组的信息,它实际上就是一个存放群组信息的数据库(Database)
2)掌握/etc/group文件每个字段的具体含义:
a)第1个字段(列):记录的是这个群组的名字
b)第2个字段(列):如果为x,表示该用户登录Linux系统时必须使用密码;如果为空,表示该用户在登录时无须提供密码
c)第3个字段(列):记录的是这个群组的gid
d)第4个字段(列)记录的是这个群组里还有哪些群组成员

(4).配置文件
在/etc/login.defs中有如下配置变量,可以用来更改此工具的行为:
CREATE_HOME(boolean) 指示是否应该为新用户默认创建主目录。此设置并不应用到系统用户,并且可以使用命令行覆盖。
GID_MAX(number),GID_MIN(number) useradd,groupadd或newusers创建的常规组的组ID范围。GID_MIN默认值1000(CentOS6为500),GID_MAX默认值60000。
MAIL_DIR(string)邮箱目录。修改或删除用户账户时需要处理邮箱,如果没有指定,将使用编译时指定的默认值。
MAIL_FILE(string) 定义用户邮箱文件的位置(相对于主目录)。
注意:MAIL_DIR和MAIL_FILE变量由useradd,usermod和userdel用于创建、移动或删除用户邮箱。如果MAIL_CHECK_ENAB设置为yes,它们也被用于定义MAIL环境变量。
MAX_MEMBERS_PER_GROUP(number) 每个组条目的最大成员数。达到最大值时,在/etc/group开始一个新条目(行)(使用同样的名称,同样的密码,同样的GID)。默认值是0,意味着组中的成员数没有限制。此功能(分割组)允许限制组文件中的行长度。这对于确保NIS组的行比长于1024字符。如果要强制这个限制,可以使用25。
注意:分割组可能不受所有工具的支持(甚至在Shadow工具集中)。如果没有必要你不应该使用这个变量。
PASS_MAX_DAYS(number) 一个密码可以使用的最大天数。如果密码比这个旧,将会强迫更改密码。如果不指定,就假定为-1,这会禁用此限制。
PASS_MIN_DAYS(number) 两次更改密码时间最小间隔。将会拒绝任何早于此的更改密码的尝试。如果不指定就假定为-1,将会禁用此限制。
PASS_WARN_AGE(number) 密码过期之前鬼畜警告的天数。0表示在过期当天警告,负值表示不警告。如果没有指定,不会给出警告。
SYS_GID_MAX(number),SYS_GID_MIN(number) useradd、groupadd或newusers创建的系统组的组ID范围。SYS_GID_MIN默认101(CentOS6为201),SYS_GID_MAX默认GID_MIN-1。
SYS_UID_MAX(number),SYS_UID_MIN(number) useradd或newusers创建的系统用户的用户ID的范围。SYS_UID_MIN默认101(CentOS6为201),SYS_UID_MAX默认UID_MIN-1。
UID_MAX(number),UID_MIN(number) useradd或newusers创建的普通用户的用户ID的范围。UID_MIN默认1000(CentOS6为500),UID_MAX默认60000。
UMASK(number) 文件模式创建掩码初始化为此值。如果没有指定,掩码初始化为022。Useradd和newusers使用此掩码设置它们创建的用户主目录的模式。也被login用于指定用户的初始umask。注意,此掩码可以被用户的GECOS行覆盖(当设置了QUOTAS_ENAB时),也可以被带K指示符的limits(5)定义的限制值覆盖。
USERGROUPS_ENAB(boolean) 如果uid和gid相同,用户名和主用户名也相同,使非root组的组掩码位和属主位相同(如:022->002,077->007)。如果设置为yes,组中也没有成员了,userdel将移除此用户组,useradd创建用户时,也会创建一个同名的默认组。

1./etc/passwd

root@root:~# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
admin:x:3:3:admin:/dev:/usr/sbin/nologin

字段与字段之间以:隔开,每个字段代表的意思如下:

1.用户名。

2.用户的密码占位符。(之前用户的密码原本存储在该字段,处于安全考虑,最后专门有了/etc/shadow文件,现在默认均由x替代)

3.用户UID值,一般情况下,root的UID为0,1-499默认为系统账号,有的更大些到1000,500-65535为用户的可登录账号,有的系统从1000开始。

4.用户的gid,linux的用户都会有两个ID,一个是用户uid,一个是用户组id。(在我们登录的时候,输入用户名和密码,首先到/etc/passwd查看是否有你输入的账号或者用户名,有的话将该账号与对应的UID和GID(在/etc/group中)读出来。然后读出主文件夹与shell的设置,然后再去检验密码是否正确,正确的话正常登录)

5.用户的账号说明解释。

6.用户的家目录文件夹。

7.用户使用的shell,如果换成/sbin/nologin时默认是没有登录环境的。

2./etc/shadow

复制代码
复制代码
root@www:~# cat /etc/shadow
root:$6$X9iEgIhv$wGtrUhjiNRp72LgCTzC1qdi.myfSU9S5nqsfr.m1KInUFoeOUBN73AmMy5sTe91biJLdHgsoimZQzeD9EvGlv1:16769:0:99999:7:::
daemon:*:16547:0:99999:7:::
bin:*:16547:0:99999:7:::
sys:*:16547:0:99999:7:::
sync:*:16547:0:99999:7:::
games:*:16547:0:99999:7:::
man:*:16547:0:99999:7:::
复制代码
复制代码

字段与字段之间以:隔开,每个字段代表的意思如下:

1.用户名,与/etc/passwd中的第一个字段一致。

2.用户的密码加密字段(“该字段存放的是加密后的用户口令字,如果为空,则对应用户没有口令,登录时不需要口令; 星号代表帐号被锁定; 双叹号表示这个密码已经过期了。
$6
$开头的,表明是用
SHA-
512加密的,
$1
$ 表明是用
MD5加密的
$2
$ 是用
Blowfish加密的
$5
$ 是用
SHA-
256加密的。)

3.“最后一次修改时间”表示的是从某个时刻起,到用户最后一次修改口令时的天数。(一般
这个时间起点是
1970年
1月
1日)

4.“最小时间间隔”指的是两次修改口令之间所需的最小天数。

5.“最大时间间隔”指的是口令保持有效的最大天数。

6.“警告时间”字段表示的是从系统开始警告用户到用户密码正式失效之间的天数。

7.“不活动时间”表示的是用户没有登录活动但账号仍能保持有效的最大天数。

8.“失效时间”字段给出的是一个绝对的天数,如果使用了这个字段,那么就给出相应账号的生存期。期满后,该账号就不再是一个合法的账号,也就不能再用来登录了。

9.最后一个字段,为保留字段,暂时没有任何含义。

AC自动机

前置知识

使用场景

AC自动机是一种著名的多模式匹配算法。

可以完成类似于KMP算法的工作,但是由单字符串的匹配变成了多字符串的匹配。

一般来说,会有很多子串,和一个母串。问题常是求字串在母串中的出现情况(包括位置,次数,等等)

算法思想与流程

我在
Trie树
一文中提到过这样一句话

而AC自动机的核心就在于通过对Trie树进行处理,使得在处理母串的信息时可以快速的进行状态转移。

可以类比KMP的算法流程,但是这不重要

例如子串有
aa
,
ab
,
abc
,
b
。母串为
ababcba

由于我们是通过母串进行状态转移,所以需要先把所有字串的信息搞定

我们可以先处理子串,建一棵Trie树

明显,对于一个字串的匹配,是不可能在树上一路到底的,所以要构建匹配失败时的回退机制。也就是需要构建失配指针。

那么失配指针是干什么的?也就是用来在
Trie
树上向上跳,找到可以转移的一个节点,进行状态转移。

假如我现在在3号节点,并且我下一个需要转移的状态是
b
,很明显,我此时应该回退到1节点(其上第一个可以通过
b
转移的节点)并转移到4节点。如果再来一个
b
,也只能向上走到0号节点,然后转移到2号节点。

如此看来,我们完全可以暴力向上跳找到可转移的状态或者到达根为止。但是,这明显不够优秀,我们完全可以继承其子节点的。也就是继承
fail
的子节点。使得不需要暴力向上跳。

那说了半天,
fail
到底指向啥?

假设父节点到当前节点转移的状态为
x
,父节点之上第一个可以通过
x
转移到下一个节点的节点为
u
,则
fail
指向
u
通过
x
转移过后的节点。

其实还有另一种解释的方法

fail
指向
p
代表当前串的最长已知后缀。

例如
aa
的最长已知后缀为
a
,所以 3号节点的
fail
指向 1号节点;
abc
的最长已知后缀为空,所以
5
号节点的
fail
指向根节点。

好混乱,我尽力了……

那么核心代码……就是利用
BFS
来处理

void procFail(int * q) {
    int head(0), tail(0);
    for (int i(0); i < 26; ++i) {
        if (kids[0][i]) q[tail++] = kids[0][i];
    }

    while (head ^ tail) {
        int x = q[head++];
        for (int i(0); i < 26; ++i) {
            if (kids[x][i]) {
                fail[kids[x][i]] = kids[fail[x]][i];
                q[tail++] = kids[x][i];
            } else kids[x][i] = kids[fail[x]][i];
        }
    } // procFail end
}

注意事项
:一般来说,把
0
号作为根节点会比较方便。反正
0
上不可能有信息保存。

插入部分我就不需要讲了

匹配的判断

如何判断当前状态有没有匹配任何一个字串,只需要不断向上跳
fail
,看跳到的节点是不是代表着字串。

拿模板:
【模板】AC 自动机(简单版) - 洛谷
为例。

插入的时候在最后标记一下有没有匹配:

void insert(string &s) {
    int p(0);
    for (int c : s) {
        if (!kids[p][(c -= 'a')]) kids[p][c] = ++usage;
        p = kids[p][c];
    }
    ++cnt[p];
}

在匹配的时候暴力跳就是了:

int ACMatch(string & s) {
    int p(0), ans(0);
    for (int c : s) {
        p = kids[p][(c -= 'a')];
        for (int t(p); t && ~cnt[t]; t = fail[t]) {
            ans += cnt[t], cnt[t] = -1;
        }
    }
    return ans;
}

由于每一个串只能匹配一次,所以这里采用的清空的策略。并且标记清空,以免重复搜索。

失配树的应用

就拿模板题来说吧:
【模板】AC 自动机(二次加强版) - 洛谷

他是要求所有字串的出现情况。

那么,我们先把每一个到达的状态计数。再通过
fail
指针向上跳求和。

但毕竟不能每一个节点都暴力跳,所以考虑在
fail
树上求和。

但是,我们不是有一个
q

BFS
吗?其中的
fail
是有序的:对于一个节点
x
,其
fail
一定在
x
之前被遍历到。

所以我们直接使用
q
即可。

那么合起来大概也就是这样:

inline void ACMatch(string &s) {
    int p(0);
    for (char c : s) {
        p = kids[p][c - 'a'];
        ++cnt[p];
    }
}

inline void ACCount(int * q) {
    for (int i = usage; i; --i) {
        cnt[fail[q[i]]] += cnt[q[i]];
    }
}

但是每一个特定的字串出现的次数呢?

在插入时记住字串对应的节点,输出即可。

void insert(string &s, int i) {
    int p(0);
    for (int c : s) {
        if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
        p = kids[p][c];
    }
    pos[i] = p;
}


inline void ACOutput(int n) {
    for (int i = 1; i <= n; ++i) {
        cout << cnt[pos[i]] << '\n';
    }
}


有这么一道题:

很明显,对于每一个位置,我们需要清理能匹配到的最长长度,所以我们需要预处理出最长长度:

inline void ACprepare(int * q) {
    for (int i = 1; i <= usage; ++i) {
        len[q[i]] = max(len[q[i]], len[fail[q[i]]]);
    }
}

在清理时:

inline void ACclean(string &s) {
    int p(0);
    for (unsigned i(0), ie = s.size(); i < ie; ++i) {
        p = kids[p][discrete(s[i])];
        if (len[p]) for (unsigned j = i - len[p] + 1; j <= i; ++j)
            s[j] = '*';
    }
}

由于是引用的字符串,所以可以直接修改。

对状态的理解

在我们考试的时候有这么一道题:

这道题说难也难,说不难也不难。主要是看对于
AC自动机
状态转移的理解到不到位。

在匹配过程中,如果匹配到了出现的
w
,那么就要回到
len(w)
个状态前,继续匹配下一个字符。

很明显,需要用栈,并且由于需要一次弹出多个,所以最好用手写的栈。

核心代码如下:

string sub, pat;
cin >> sub >> pat;
insert(sub), procFail(Q);

int p = 0;
for (int i(0), ie = pat.size(); i < ie; ++i) {
    p = kids[cps[ci]][pat[i] - 'a'];
    cps[++ci] = p, ccs[ci] = pat[i];
    if (match[p]) ci -= sub.size();
}

for (int i = 1; i <= ci; ++i) {
    putchar(ccs[i]);
}

这里没有用到
fail
,那么为什么还要构建失配树?

这是个好问题,因为,构建失配树的过程不仅仅构建了失配树,同时还令节点继承了其
fail
的子节点,所以需要构建的过程。


最后附上模板题
【模板】AC 自动机(二次加强版) - 洛谷
的代码:

#include <iostream>
#include <algorithm>
#include <string>

using namespace std;
const int N = 1e6 + 7;

int res[N], cnt[N], pos[N];
class ACAutomaton {
private:
	int kids[N][26];
	int fail[N], id[N], usage;
public:
	ACAutomaton() : usage(0) {
	}
	
	inline int newNode() {
		fill_n(kids[++usage], 26, 0);
		cnt[usage] = fail[usage] = id[usage] = 0;
		return usage;
	}
	
	void insert(string &s, int i) {
		int p(0);
		for (int c : s) {
			if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
			p = kids[p][c];
		}
		pos[i] = p;
	}
	
	void procFail(int * q) {
		int head(0), tail(0);
		for (int i(0); i < 26; ++i) {
			if (kids[0][i])
				fail[kids[0][i]] = 0, q[tail++] = kids[0][i];
		}
		
		while (head ^ tail) {
			int x = q[head++];
			for (int i(0); i < 26; ++i) {
				if (kids[x][i]) {
					fail[kids[x][i]] = kids[fail[x]][i];
					q[tail++] = kids[x][i];
				} else kids[x][i] = kids[fail[x]][i];
			}
		} // procFail end
	}
	
	void debug() {
		for (int i = 0; i <= usage; ++i) {
			printf("node %d (cnt %d) fail to %d:\n\t", i, cnt[i], fail[i]);
			for (int j(0); j < 26; ++j) {
				printf("%d ", kids[i][j]);
			} puts("");
		}
	}
	
	inline void ACMatch(string &s) {
		int p(0);
		for (char c : s) {
			p = kids[p][c - 'a'];
			++cnt[p];
		}
	}
	
	inline void ACCount(int * q) {
		for (int i = usage; i; --i) {
			cnt[fail[q[i]]] += cnt[q[i]];
		}
	}
	
	inline void ACOutput(int n) {
		for (int i = 1; i <= n; ++i) {
			cout << cnt[pos[i]] << '\n';
		}
	}
	
	void clear() {
		usage = -1;
		newNode(); // clear 0
	}
} ac;

int Q[N];
string s; 

int main() {
	cin.tie(0)->sync_with_stdio(false);
	
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> s;
		ac.insert(s, i);
	} ac.procFail(Q);
	
	cin >> s;
	ac.ACMatch(s);
	ac.ACCount(Q);
	ac.ACOutput(n);
	return 0;
}

差不多了……下课

网络中的图片传输

前言

一张图片经过网络从主机 A 传输到主机 B,主机 B 在收到这张图片后将其保存在本地,对应步骤为:

  1. 读:主机 A 读取待传输的图片数据
  2. 传:主机 A 通过 Socket 将图片传输给主机 B
  3. 写:主机 B 在收到图片数据后,将其保存在本地

我们来思考这样几个问题:

  1. 图片数据要以怎样的形式在网络中进行传输?
  2. 对端收到数据后怎要确保是否接收完毕?
  3. 怎样确保图片文件可以在网络上正确传输?

为解决这些问题,我们可以从发送的数据格式入手,收发双方约定使用如下格式进行数据传输:

POST /Picture HTTP/1.1
Host: IP:端口号
Content-Length: 数据长度

数据内容

而对于数据内容,可以考虑使用 JSON 格式:

{
    "imageName" : "test.png",
    "imageSize" : 4,
    "imageData" : "abcd"
}

这样就构成了一条数据,以主机 A(192.168.3.60) 向主机 B(192.168.3.66) 的 5073 端口发送数据为例,其完整格式为:

POST /Picture HTTP/1.1
Host: 192.168.3.66::5073
Content-Length: 83

{
    "imageName" : "test.png",
    "imageSize" : 4,
    "imageData" : "abcd"
}

主机 B 在收到主机 A 数据后,根据报文头部的长度 + Content-Length 对应的值,便可以轻松得到此次接收的数据总长度。全部接收完毕后将 imageData 值解析出来保存在本地即可,而对于 JSON 字符的解析操作,可以考虑使用轻量级的 cJSON 解析器。

但是还有一个问题,我们知道,在一张图片数据中存在大量的不可见字符,当不可见字符在网络上传输时,往往要经过多个路由设备,由于不同的设备对不可见字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。

那么怎样确保图片数据被正确传输了呢?答案就是使用 Base64。

接下来我们就「图片读写操作、Base64、cJSON 和 Socket 编程」来完成网络中图片的传输。

一、图片读写操作

在正式开始图片读写之前,我们先来看下与文件读写相关的一些函数。

1.1 fopen 和 fclose函数

1.1.1 fopen 函数介绍

函数原型:
FILE *fopen( const char *fileName, const char *mode );

参数介绍:

  1. fileName:文件名,可以包含路径和文件名两部分

  2. mode:表示打开文件的类型,关于文件类型的规定参见下表:


    访问模式 描述
    r 打开一个已有的文本文件,允许读取文件
    w 打开一个文本文件,允许写入文件
    如果文件存在,则该文件会被截断为零长度,重新写入
    如果文件不存在,则会创建一个新文件
    a 打开一个文本文件,以追加模式写入文件
    如果文件不存在,则会创建一个新文件
    r+ 打开一个文本文件,允许读写文件
    w+ 打开一个文本文件,允许读写文件
    如果文件已存在,则文件会被截断为零长度,重新写入
    如果文件不存在,则会创建一个新文件
    a+ 打开一个文本文件,允许读写文件
    如果文件不存在,则会创建一个新文件
    读取会从文件的开头开始,写入则只能是追加模式。

    如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

    • "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

返 回 值:
如果成功的打开一个文件,返回文件指针;否则返回空指针。

1.1.2 fclose 函数介绍

函数原型:
int fclose(FILE *fp);

fclose
函数用来关闭一个由
fopen
函数打开的文件。该函数返回一个整型数:

  • 当文件关闭成功时,返回0
  • 否则返回一个非零值
FILE *fp = fopen(fileName, "r");
fclose(fp);

1.2 fseek 和 ftell 函数

对于文件的读写方式,C 语言不仅支持简单地顺序读写方式,还支持随机读写(即只要求读写文件中某一指定的部分)。相比于顺序读写,随机读写需要将文件指针移动到需要读写的位置再进行读写操作,这通常也被称为文件的定位。

对于文件的定位,可以通过
fseek

ftell
函数来完成。

1.2.1 fseek 函数介绍

函数原型:
int fseek(FILE *fp, long offset, int whence);

参数介绍:

  1. fp:文件指针

  2. offset:偏移量,表示要移动的字节数。正数表示正向偏移,负数表示负向偏移

  3. whence:表示设定从文件的哪里开始偏移,取值范围如下表所示


    起始点
    文件首 SEEK_SET 0
    当前位置 SEEK_CUR 1
    文件末尾 SEEK_END 2

返 回 值:

  • 如果该函数执行成功则返回 0,并将 fp 指向以 whence 为基准,偏移 offset 个字节的位置
  • 如果该函数执行失败则返回 -1,并设置 errno 的值,但并不改变 fp 指向的位置

通过 offset 和 whence 参数,可精准调节文件指针的位置。

/*将读写位置正向偏移至离文件开头 100 字节处*/
fseek(fp, 100L, SEEK_SET);

/*将读写位置正向偏移至离文件当前位置 100 字节处*/
fseek(fp, 100L, SEEK_CUR);

/*将读写位置负向偏移至离文件结尾 100 字节处*/
fseek(fp, -100L, SEEK_END);

/*将读写位置移动到文件的起始位置*/
fseek(fp, 0L, SEEK_SET);

/*将读写位置移动到文件尾*/
fseek(fp, 0L, SEEK_END);

1.2.2 ftell 函数介绍

函数原型:
long ftell(FILE *fp);

参数介绍:
fp:文件指针

返 回 值:
该函数用于得到文件指针当前位置相对于文件首的偏移字节数。

通过联动
fseek

ftell
可以很方便的获取文件大小:

long GetFileLength(FILE *fp)
{
    long curpos = 0L;
    long length = 0L;

    curpos = ftell(fp);             // 保存fp相对于文件首的偏移量
    fseek(fp, 0L, SEEK_END);        // 将fp移动到文件尾
    length = ftell(fp);             // 统计文件大小
    fseek(fp, curpos, SEEK_SET);    // 将fp归位
    
    return length;
}

1.3 fread 和 fwrite 函数

1.3.1 fread 函数介绍

函数原型:
size_t fread(void *buffer, size_t size, size_t count, FILE *fp);

参数介绍:

  1. buffer:读入数据的存储地址
  2. size:每个数据的大小,单位是字节
  3. count:读取的数据个数
  4. fp:待读取的文件指针

返 回 值:
fread()
返回实际读取的元素个数

Notes:

  1. fread 可以读二进制文件
  2. 可通过比较实际读取的元素个数和预想的个数,来判断文件是否被正确读取。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

long GetFileLength(FILE *fp)
{
    long curpos = 0L;
    long length = 0L;

    curpos = ftell(fp);          // 保存fp相对于文件首的偏移量
    fseek(fp, 0L, SEEK_END);     // 将fp移动到文件尾
    length = ftell(fp);          // 统计文件大小
    fseek(fp, curpos, SEEK_SET); // 将fp归位

    return length;
}
int main()
{
    FILE *fp = fopen("test.txt", "rb+"); // test.txt中的文件内容为:0123456789

    // 获取文件大小
    int length = GetFileLength(fp); // length = 10

    // 申请一块能装下整个文件的空间
    char *buffer = (char *)malloc(sizeof(char) * length);
    int size = sizeof(char);   // 每次读取1个字节
    int count = length / size; // 读取10次

    int readLen = fread(buffer, size, count, fp); // 如果readLen=count=10,则读取成功
    if (readLen != count) // 判断实际读取的元素个数readLen和预想的个数count是否相等
    {
        printf("fread error.\n");
    }

    printf("[%s](%d)\n", buffer, readLen);
    fclose(fp);
    return 0;
}

1.3.2 fwirte 函数介绍

函数原型:
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *fp);

参数介绍:

  1. buffer:指向数据块的指针
  2. size:每个元素的大小,单位是字节
  3. count:写入的数据个数
  4. fp:待写入的文件指针

返 回 值:
成功写入则返回实际写入的数据个数,
fwrite
的返回值随着调用格式的不同而不同。

  • 调用格式一:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        FILE *fp = fopen("test.txt", "wb+");
    
        char buffer[] = "0123456789";
        int bufLen = strlen(buffer);    // bufLen = 10
    
        int size = sizeof(char);        // 每次写入1个字节
        int count = bufLen / size;      // 写入10次
        int writeLen = fwrite(buffer, size, count, fp); // writeLen = count = 10
    
        fclose(fp);
        return 0;
    }
    
  • 调用格式二:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        FILE *fp = fopen("test.txt", "wb+");
    
        char buffer[] = "0123456789";
        int bufLen = strlen(buffer);    // bufLen = 10
    
        int size = bufLen;              // 每次写入bufLen个字节,即将buffer一次性写入
        int count = bufLen / size;      // 写入1次
        int writeLen = fwrite(buffer, size, count, fp); // writeLen = count = 1
    
        fclose(fp);
        return 0;
    }
    

1.4 图片读写

1.4.1 readAndwrite.h

#ifndef __READANDWRITE_H__
#define __READANDWRITE_H__

int Read(const char *fileName, char **buffer);
int Write(const char *fileName, char *buffer, int length);

#endif 

1.4.2 readAndwrite.c

#include <stdio.h>
#include <stdlib.h>
#include "readAndwrite.h"

/********************************************************
 * 函数功能:获取文件大小
 * 参数说明:fp 入参,表示文件指针
 * 返 回 值:返回fp所指向的文件大小
 *******************************************************/
static int GetFileLength(FILE *fp)
{
    long curpos = 0L;
    long length = 0L;

    curpos = ftell(fp);             // 保存fp相对于文件首的偏移量
    fseek(fp, 0L, SEEK_END);        // 将fp移动到文件尾
    length = ftell(fp);             // 统计文件大小
    fseek(fp, curpos, SEEK_SET);    // 将fp归位
    
    return (int)length;
}

/********************************************************
 * 函数功能:以二进制形式读文件
 * 参数说明:fileName 入参,表示待读取的文件
 *          buffer   出参,将读取的文件保存在buffer中
 * 返 回 值:读取成功则返回读取的文件大小,失败返回 0
 *******************************************************/
int Read(const char *fileName, char **buffer)
{
    if (fileName == NULL || buffer == NULL)
    {
        printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
        return 0;
    }

    FILE *fp = fopen(fileName, "rb");
    if (fp == NULL)
    {
        printf("[%s][%s-%lu] Open fail(%s) error.\n", __FILE__, __FUNCTION__, __LINE__, fileName);
        return 0;
    }

    int length = GetFileLength(fp);

    // 申请一块能装下整个文件的空间
    (*buffer) = (char *)malloc(sizeof(char) * (length + 1));
    int size = fread(*buffer, sizeof(char), length, fp);
    if (size != length) // 通过比较实际读取长度size和预期长度length,来判断是否读取成功
    {
        printf("[%s][%s-%lu] Fail to call fread.\n", __FILE__, __FUNCTION__, __LINE__);
        fclose(fp);
        return 0;
    }

    fclose(fp);
    return size;
}

/********************************************************
 * 函数功能:以二进制形式写文件
 * 参数说明:fileName 入参,表示文件写入的路径
 *          buffer   入参,表示待写入的文件
 *          len      入参,表示buffer的大小
 * 返 回 值:写入成功则返回实际写入的长度,失败返回 0
 *******************************************************/
int Write(const char *fileName, char *buffer, int length)
{
    if (fileName == NULL || buffer == NULL || length <= 0)
    {
        printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
        return 0;
    }
    FILE *fp = fopen(fileName, "wb+");
    if (fp == NULL)
    {
        printf("[%s][%s-%lu] Open fail(%s) error.\n", __FILE__, __FUNCTION__, __LINE__, fileName);
        return 0;
    }

    int size = fwrite(buffer, sizeof(char), length, fp);
    if (size != length) // 通过比较实际写入长度size和预期长度length,来判断是否写入成功
    {
        printf("[%s][%s-%lu] Fail to call fwrite.\n", __FILE__, __FUNCTION__, __LINE__);
        fclose(fp);
        return 0;
    }
    fclose(fp);

    return size;
}

1.4.3 testReadAndWrite.c

#include <stdio.h>
#include <stdlib.h>
#include "readAndwrite.h"

#define FILE_READ_NAME "./image/wallpaper.png"
#define FILE_WRITE_NAME "./image/wallpaper_copy.png"

int main()
{
    char *buffer;
    int readLen = Read(FILE_READ_NAME, &buffer);
    if (readLen == 0)
    {
        printf("[%s][%s-%lu] Read error.\n", __FILE__, __FUNCTION__, __LINE__);
        exit(0);
    }
    else
    {
        printf("[%s][%s-%lu] Read succeed.\n", __FILE__, __FUNCTION__, __LINE__);
    }

    int writeLen = Write(FILE_WRITE_NAME, buffer, readLen);
    if (writeLen == 0)
    {
        printf("[%s][%s-%lu] Write error.\n", __FILE__, __FUNCTION__, __LINE__);
        exit(0);
    }
    else
    {
        printf("[%s][%s-%lu] Write succeed.\n", __FILE__, __FUNCTION__, __LINE__);
    }

    return 0;
}

1.4.4 Tutorial

目录结构:

image-20230326122324487

  1. 将 readAndwrite.h、readAndwrite.c 和 testReadAndWrite.c 置于 ReadingAndWriting 目录下。

  2. 在 image 目录下存在一张图片 wallpaper.png

  3. 编译、运行

    image-20230326122504921

通过打印的日志信息可以看出,图片读写都成功了,下面我们通过文件树看一下是否真的成功了:

image-20230326122547269

最后对比一下这两个文件的 md5sum 值:

image-20230326123200079

二、Base64

2.1 何为 Base64

Base64 是一种基于 64 个可打印字符来表示二进制数据的方法,这 64 个可打印字符包括:

  1. 大写字母
    A~Z
  2. 小写字母
    a~z
  3. 数字
    0~9
  4. +

    /

2.2 为什么要使用 Base64

我们知道一个字节(1B = 8b)可表示的范围是 0~255, 其中 ASCII 值的范围为 0~127(十六进制:0x00~0x7F),而超过 ASCII 范围的 128~255 之间的值是不可见字符。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,它主要用于显示现代英语。

在 ASCII 码中 0~31 和 127 是控制字符,共 33 个。以下是其中一部分控制字符:

img

其余 95 个,即 32~126 是可打印字符,包括数字、大小写字母、常用符号等:

img

当不可见字符在网络上传输时,往往要经过多个路由设备,由于不同的设备对不可见字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。

而图片文件中就包含大量的不可见字符,所以我们想要在网络中正确传递图片,就可以考虑使用 Base64:

  • 对于待传输的图片数据,可通过 Base64 将其编码为可见字符在网络中传输
  • 对端收到经 Base64 编码的数据后,通过 Base64 编码的逆过程,将其解码为原图片

2.3 Base64 详解

2.3.1 前置知识

通过 2.1 我们知道,Base64 是一种基于 64 个可打印字符来表示二进制数据的方法。由于
\(64=2^{6}\)
,所以一个 Base64 字符实际上代表着 6 个二进制位(bit,比特)。

在二进制数据中,1 个字节对应的是8比特(1B = 8b),而 3 个字节有 24 个比特,正好对应于 4 个 Base64 字符,即 3 个字节可由 4 个 Base64 字符来表示,相应的转换过程如下图所示:

img

前面 2.1 我们也提到了,Base64 包含 64 个可打印字符,相应的索引表如下:

img

等号
=
用来作为后缀用途。

2.3.2 Base64 编码

了解完上述的知识,我们以编码字符串
you
为例,来直观的感受一下编码过程。

具体的编码方式:

  1. 将每 3 个字节作为一组,3 个字节一共 24 个二进制位
  2. 将这 24 个二进制位分为 4 组,每个组有 6 个二进制位,对应于 6 个 Base64 字符
  3. 每个 Base64 字符对应的将是一个小于 64 的数字,即为字符编号
  4. 最后根据索引表(图 4),就得到了经 Base64 编码后的字符串

img

  • 由图可知,
    you
    (3 字节)编码的结果为
    eW91
    (4字节)
  • 很明显经过 Base64 编码后体积会增加 1/3

由于
you
这个字符串的长度刚好是 3B,我们可以用 4 个 Base64 字符来表示。但如果待编码的字符串长度不是 3 的整数倍时,应该如何处理呢?

如果要编码的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,那么可以使用下面的方法进行处理:先使用 0 字节值在末尾补足,使其能够被 3 整除,然后再进行 Base64 的编码。

以编码字符
A
为例,其所占的字节数为 1,不能被 3 整除,需要补 2 个 0 字节,具体如下图所示:

img

  • 字符
    A
    经过 Base64 编码后的结果是
    QQ==
  • 该结果后面的两个
    =
    代表补足的字节数

接着我们来看另一个示例,假设需编码的字符串为 BC,其所占字节数为 2,不能被 3 整除,需要补 1 个 0 字节,具体如下图所示:

img

  • 字符串
    BC
    经过 Base64 编码后的结果是
    QkM=
  • 该结果后面的 1 个
    =
    代表补足的字节数

2.4 Base64 编解码

2.4.1 base64.h

#ifndef __BASE64_H__
#define __BASE64_H__

char *Base64Encode(const char *str, int len, int *encodedLen);
int Base64Decode(const char *base64Encoded, char **base64Decoded);

#endif

2.4.2 base64.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "base64.h"

// 定义base64编码表
static const char base64EncodeTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/********************************************************
 * 函数功能:计算经过base64编码后的新字符串的长度
 * 参数说明:len 入参,表示待编码的字符串的长度
 * 返 回 值:返回经base64编码后的新字符串的长度
 *******************************************************/
static int Base64EncodeLen(int len)
{
    return (((len + 2) / 3) * 4);
}

/********************************************************
 * 函数功能:base64编码,返回经base64编码后的字符串
 * 参数说明:str 入参,表示待编码的字符串
 *          len 入参,表示待编码的字符串的长度
 *          encodedLen 出参,保存编码后的字符串的长度
 * 备   注:因str可能包含不可见字符及'\0',所以参数len是必须的
 * 返 回 值:返回经base64编码后的字符串
 *******************************************************/
char *Base64Encode(const char *str, const int len, int *encodedLen)
{
    char *encoded = (char *)malloc(Base64EncodeLen(len) + 1);
    char *p = encoded;

    // str中,每3位为一组,经过base64后变成4位
    int i;
    for (i = 0; i < len - 2; i += 3)
    {
        // 取出第一个字符的前6位并找出对应的结果字符
        *p++ = base64EncodeTable[(str[i] >> 2) & 0x3F];

        // 将第一个字符的后2位与第二个字符的前4位进行组合并找到对应的结果字符
        *p++ = base64EncodeTable[((str[i] & 0x3) << 4) | ((str[i + 1] & 0xF0) >> 4)];

        // 将第二个字符的后4位与第三个字符的前2位组合并找出对应的结果字符
        *p++ = base64EncodeTable[((str[i + 1] & 0xF) << 2) | ((str[i + 2] & 0xC0) >> 6)];

        // 取出第三个字符的后6位并找出结果字符
        *p++ = base64EncodeTable[str[i + 2] & 0x3F];
    }
    if (i < len) // 如果 i < len,说明 i % 3 != 0,需要额外补充 '='
    {
        *p++ = base64EncodeTable[(str[i] >> 2) & 0x3F];
        if (i == (len - 1)) // 剩余一个字符
        {
            *p++ = base64EncodeTable[((str[i] & 0x3) << 4)];
            *p++ = '=';
        }
        else if (i == len - 2) // 剩余两个字符
        {
            *p++ = base64EncodeTable[((str[i] & 0x3) << 4) | ((int)(str[i + 1] & 0xF0) >> 4)];
            *p++ = base64EncodeTable[((str[i + 1] & 0xF) << 2)];
        }
        *p++ = '=';
    }
    *p = '\0';
    *encodedLen = p - encoded;

    return encoded;
}

// 定义base64解码表,并将base64DecodeTable['=']置为0,便于统一处理编码后存在'='号的情况
//根据 base64 编码表,以字符找到对应的十进制数据 
static const unsigned char base64DecodeTable[] = 
{
    64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
    64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
    64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
    64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
    64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 
    54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 
    64,  0, 64, 64, 64,  0,  1,  2,  3,  4, 
     5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 
    25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 
    29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 
    39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 
    49, 50, 51, 64, 64, 64, 64, 64, 64, 64
};

/********************************************************
 * 函数功能:计算经base64解码后的字符串的最大长度
 * 参数说明:encoded 入参,表示经base64编码后的字符串
 *          len 出参,用于保存encoded的长度
 * 返 回 值:返回经base64解码后的新字符串的最大长度
 * 备    注:忽略'='的影响
 *******************************************************/
static int Base64DecodeLen(const char *encoded, int *len)
{
    register const char *bufin = encoded;    // 声明寄存器变量:直接存储在CPU中的寄存器中的变量,频繁调用时提高运行效率
    for (; base64DecodeTable[*bufin] <= 63;) // base64DecodeTable['\0'] = 64,该函数的作用其实等价于 strlen(encoded)
    {
        bufin++;
    }
    *len = bufin - encoded; // 获取encoded的字符长度(包含'=')
    return (*len / 4) * 3;
}

/********************************************************
 * 函数功能:base64解码
 * 参数说明:base64Encoded 入参,表示经base64编码后的字符串
 *          base64Decoded 出参,用于保存解码后的字符串
 * 返 回 值:返回经base64解码后的字符串的实际长度
 * 备    注:1. 考虑'='的影响
 *           2. 由于经base64解码后的字符串可能包含不可见字符及'\0',所以是有必要返回解码后的字符串长度的
 *******************************************************/
int Base64Decode(const char *base64Encoded, char **base64Decoded)
{
    int len;
    int decodedLen = Base64DecodeLen(base64Encoded, &len);
    if (len <= 0 || len % 4 != 0) // base64Encoded必须非空且长度为4的整倍数,才能进行后续的解码操作
    {
        printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
        return 0;
    }

    char *decoded = (char *)malloc(decodedLen + 1);
    decoded[decodedLen] = 0;

    int i;
    char *bufout = decoded;
    for (i = 0; i + 3 < len; i += 4) // 以4个字符为一组进行解码
    {
        // 取出当前组的「第1个字符对应base64解码表的十进制数的后六位」与「第2个字符对应base64解码表的十进制数的前两位」进行组合
        *bufout++ = (char)(base64DecodeTable[base64Encoded[i]] << 2 | base64DecodeTable[base64Encoded[i + 1]] >> 4);

        // 取出当前组的「第2个字符对应base64解码表的十进制数的后四位」与「第3个字符对应bas464解码表的十进制数的前四位」进行组合
        *bufout++ = (char)(base64DecodeTable[base64Encoded[i + 1]] << 4 | base64DecodeTable[base64Encoded[i + 2]] >> 2);

        // 取出当前组的「第3个字符对应base64解码表的十进制数的后两位」与「第4个字符对应bas464解码表的十进制数的前六位」进行组合
        *bufout++ = (char)(base64DecodeTable[base64Encoded[i + 2]] << 6 | base64DecodeTable[base64Encoded[i + 3]]);
    }

    *base64Decoded = decoded;

    if (base64Encoded[len - 2] == '=')
        decodedLen -= 2; // 存在两个'=',则实际长度 -2
    else if (base64Encoded[len - 1] == '=')
        decodedLen -= 1; // 存在一个'=',则实际长度 -1

    return decodedLen; // 返回解码后的实际长度
}

三、cJSON

对于 cJSON 的介绍,详见我的这篇博客:
cJson 学习笔记 - MElephant - 博客园 (cnblogs.com)

四、Socket

有关 Socket 的介绍,详见我的这篇博客:
Socket 编程 - MElephant - 博客园 (cnblogs.com)

4.1 socket.h

#ifndef __SOCKET_H__
#define __SOCKET_H__

#define BIT0 (0x1 << 0)
#define BIT1 (0x1 << 1)
#define BIT2 (0x1 << 2)

typedef unsigned int BOOL;
#define TRUE    1
#define FALSE   0

#define E_SUCCEED   0
#define E_ERROR     112

#define BACKLOG 10 // 设置Socket最大监听个数

/* 定义发送 HTTP 报文格式 */
#define CLIENT_HTTP_BUF    "\
POST /Picture HTTP/1.1\r\n\
Host: %s\r\n\
Content-Length: %d\r\n\
Content-Type: image\r\n\
\r\n\
%s\r\n\
\r\n"

/* 定义响应 HTTP 报文格式 */
#define SERVER_HTTP_BUF "\
HTTP/1.1 200 OK\r\n\
Content-Length: %d\r\n\
Content-Type: text/plain\r\n\
\r\n\
%s\r\n\
\r\n"

#define HTTP_HDR_TAIL_STR "\r\n\r\n"    // 报文头结束标志
#define HTTP_HDR_LINE_TAIL_STR "\r\n"   // 行结束标志
#define HTTP_CONTENT_LENGTH_STR "Content-Length: "
#define HTTP_HDR_LEN      256           // 发送HTTP报文格式中的头部长度,多多益善

typedef enum tagSocketOpt
{
    SOCKET_OPT_BIND     = BIT0,
    SOCKET_OPT_LISTEN   = BIT1,
    SOCKET_OPT_CONNECT  = BIT2
} SOCKET_OPT_E;

typedef struct tagIpAddr
{
    char *ip;               // IP 地址,点分十进制
    unsigned short port;    // 端口号
} IPADDR_S;

int SocketCreate(int *fd, int createOpt, const IPADDR_S stIpAddr); // 创建 Socket,IPv4 & TCP
int SocketSend(int hSocket, const char *sendBuf, const int bufLen);
int SocketRecv(int hSocket, char **recvBuf, int *recvBufLen);

#endif

4.2 socket.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include "socket.h"

/********************************************************
 * 函数功能:创建基于IPv4的TCP socket
 * 参数说明:fd          出参,用于保存sockfd
 *          createOpt   入参,表示创建socket后的操作
 *          stIpAddr    入参,表示ip地址和端口号
 * 返 回 值:创建成功则返回 E_SUCCEED,否则返回 E_ERROR
 *******************************************************/
int SocketCreate(int *fd, int createOpt, const IPADDR_S stIpAddr)
{
    int hSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == hSocket)
    {
        printf("[%s][%s-%lu] Fail to call socket.\n", __FILE__, __FUNCTION__, __LINE__);
        return E_ERROR;
    }

    if (SOCKET_OPT_BIND & createOpt)
    {
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(stIpAddr.port);   // 将本地端口号转化为网络字节序
        inet_aton(stIpAddr.ip, &addr.sin_addr); // 将点分十进制的IP地址转换为网络字节序

        int iReuse = 1;
        setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, &iReuse, sizeof(iReuse)); // 设置复用socket地址
        int iBind = bind(hSocket, (struct sockaddr *)&addr, sizeof(addr));
        if (-1 == iBind)
        {
            printf("[%s][%s-%lu] Fail to call bind.\n", __FILE__, __FUNCTION__, __LINE__);
            close(hSocket);
            return E_ERROR;
        }
        printf("[%s][%s-%lu] Socket bind succeed[%s:%u].\n", __FILE__, __FUNCTION__, __LINE__, stIpAddr.ip, stIpAddr.port);
    }

    if (SOCKET_OPT_LISTEN & createOpt)
    {
        int iListen = listen(hSocket, BACKLOG);
        if (-1 == iListen)
        {
            printf("[%s][%s-%lu] Fail to call listen.\n", __FILE__, __FUNCTION__, __LINE__);
            close(hSocket);
            return E_ERROR;
        }
        printf("[%s][%s-%lu] Socket listen succeed.\n", __FILE__, __FUNCTION__, __LINE__);
    }

    if (SOCKET_OPT_CONNECT & createOpt)
    {
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(stIpAddr.port);   // 将本地端口号转化为网络字节序
        inet_aton(stIpAddr.ip, &addr.sin_addr); // 将点分十进制的IP地址转换为网络字节序
        int iConn = connect(hSocket, (struct sockaddr *)&addr, sizeof(addr));
        if (-1 == iConn)
        {
            printf("[%s][%s-%lu] Fail to call connect.\n", __FILE__, __FUNCTION__, __LINE__);
            close(hSocket);
            return E_ERROR;
        }
        printf("[%s][%s-%lu] Socket connect succeed[%s:%u].\n", __FILE__, __FUNCTION__, __LINE__, stIpAddr.ip, stIpAddr.port);
    }

    *fd = hSocket;
    return E_SUCCEED;
}

/********************************************************
 * 函数功能:发送TCP字节流
 * 参数说明:hSocket 入参,表示sockfd
 *          sendBuf 入参,表示待发送的字节流
 *          bufLen  入参,表示待发送的字节流的长度
 * 返 回 值:发送成功则返回 E_SUCCEED,否则返回 E_ERROR
 *******************************************************/
int SocketSend(int hSocket, const char *sendBuf, const int bufLen)
{
    int iSendLen = 0; // 已发送的字符个数

    while (iSendLen < bufLen)
    {
        int iRet = send(hSocket, sendBuf + iSendLen, bufLen - iSendLen, 0);
        if (iRet < 0)
        {
            if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
                continue;
            else
            {
                printf("[%s][%s-%lu] Socket Send Error.\n", __FILE__, __FUNCTION__, __LINE__);
                return E_ERROR;
            }
        }
        iSendLen += iRet;
    }

    return E_SUCCEED;
}

/********************************************************
 * 函数功能:报文头预解
 * 参数说明:buf 入参,表示当前已接收的字节流
 * 返 回 值:跟据解析buf中的Content-Length字段,返回本次需要接收的字符串总长度
 *******************************************************/
static int PreParseRecvedBuf(const char *buf)
{
    char *pcTmp = NULL;
    char *pcStart = NULL;
    char *pcEnd = NULL;
    char szContentLen[16];  // 保存Content-Length的值的字符串形式
    int iContentLen = 0;    // 保存Content-Length的值的整数形式
    int bufHeadLen = 0;

    pcTmp = strstr(buf, HTTP_HDR_TAIL_STR);
    bufHeadLen = pcTmp - buf + 4; // 本次接收的报文头部总长度,+ 4 指的是报文头的结束后的换行 \r\n\r\n

    // 找到Content-Length对应的值
    pcStart = strstr(buf, HTTP_CONTENT_LENGTH_STR);
    pcStart += strlen(HTTP_CONTENT_LENGTH_STR);
    pcEnd = strstr(pcStart, HTTP_HDR_LINE_TAIL_STR);

    strncpy(szContentLen, pcStart, pcEnd - pcStart); // 将Content-Length值复制到szContentLen中
    iContentLen = atoi(szContentLen); // 本次接收的报文的内容总长度

    return bufHeadLen + iContentLen; // 本次需要接收的报文总长度 = 头部总长度 + 内容总长度
}

/********************************************************
 * 函数功能:接收TCP字节流
 * 参数说明:hSocket 入参,表示sockfd
 *          recvBuf 出参,用于保存接收后的字节流
 *          recvBufLen 出参,用于保存接收的字节流的总长度
 * 备    注:recvBuf需要在调用该函数前开辟空间,否则在调用realloc时会报invalid next size
 * 返 回 值:接收成功则返回 E_SUCCEED,否则返回 E_ERROR
 *******************************************************/
int SocketRecv(int hSocket, char **recvBuf, int *recvBufLen)
{
    int iRecvedLen = 0;     // 已接收的字符长度
    BOOL bPreParse = FALSE; // 判断是否处理了第一次接收的128个字符

    memset(*recvBuf, 0, *recvBufLen);

    while(TRUE)
    {
        int iRet = recv(hSocket, *recvBuf + iRecvedLen, *recvBufLen - iRecvedLen, 0);
        if (iRet < 0)
        {
            if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
                continue;
            else
            {
                printf("[%s][%s-%lu] Socket Recv Error, errno(%d/%s).\n", __FILE__, __FUNCTION__, __LINE__, errno, strerror(errno));
                return E_ERROR;
            }
        }
        else if (iRet == 0)
        {
            if (iRecvedLen >= *recvBufLen) // 已接收的字符长度 ≥ 对端发送的字符总长度,说明接收完成
            {
                break;
            }
            else
            {
                printf("[%s][%s-%lu] Socket Recv Error, errno(%d/%s).\n", __FILE__, __FUNCTION__, __LINE__,  errno, strerror(errno));
                return E_ERROR;
            }
        }
        else 
        {
            iRecvedLen += iRet;

            if (bPreParse == FALSE)
            {
                // 预处理第一次接收的字符,并根据Content-Length确认此次需要接收的字符总长度
                *recvBufLen = PreParseRecvedBuf(*recvBuf);   // 从接收的HTTP头部中获取本次需要接收的报文总长度
                *recvBuf = (char *)realloc(*recvBuf, *recvBufLen + 1); // 根据需要接收的报文总长度重新为buf开辟所需长度的空间
                memset(*recvBuf + iRecvedLen, 0, *recvBufLen + 1 - iRecvedLen);

                bPreParse = TRUE;
            }
            else if (iRecvedLen < *recvBufLen)
            {
                continue;
            }

            if (iRecvedLen >= *recvBufLen)
            {
                break;
            }
        }
    }

    return E_SUCCEED;
}

五、在网络上中传输图片

5.1 common.h

#ifndef __COMMON_H__
#define __COMMON_H__

#define SAFE_FREE(ptr) \
    if (ptr) \
    { \
        free(ptr); \
        ptr = NULL; \
    }

#define FILENAME_READ "./image/wallpaper.png"
#define FILENAME_WRITE "./image/wallpaper_copy.png"

typedef struct tagImage
{
    char imageName[64]; // 图片名
    int imageSize;      // 图片大小
    char *data;         // 图片
} IMAGE_S;

#endif

5.2 Server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include "common.h"
#include "../Base64/base64.h"
#include "../CJSON/cJSON.h"
#include "../Socket/socket.h"
#include "../ReadingAndWriting/readAndwrite.h"

IPADDR_S ipAddr = {"192.168.204.128", 5073};

int Process(const char *buf, IMAGE_S *pstImage)
{
    char *tmp = strstr(buf ,"{");
    cJSON *pstRoot = cJSON_Parse(tmp);

    cJSON *pName = cJSON_GetObjectItem(pstRoot, "imageName");
    cJSON *pSize = cJSON_GetObjectItem(pstRoot, "imageSize");
    cJSON *pDataEncoded = cJSON_GetObjectItem(pstRoot, "dataEncoded");

    char *encoded = pDataEncoded->valuestring;
    char *decoded;
    int decodedLen = Base64Decode(encoded, &decoded);
    
    if (decodedLen != pSize->valueint)
    {
        printf("[%s][%s-%lu] Process error.\n", __FILE__, __FUNCTION__, __LINE__);
        return E_ERROR;
    }

    strcpy(pstImage->imageName, pName->valuestring);
    pstImage->imageSize = decodedLen;
    pstImage->data = decoded;
    
    cJSON_Delete(pstRoot);

    return E_SUCCEED;
}
int main()
{
    int iRet = E_SUCCEED;
    int hSocket;
    int opt = SOCKET_OPT_BIND | SOCKET_OPT_LISTEN;
    iRet = SocketCreate(&hSocket, opt, ipAddr);
    if (iRet != E_SUCCEED)
    {
        printf("[%s][%s-%lu] Socket create error.\n", __FILE__, __FUNCTION__, __LINE__);
        exit(0);
    }
    
    int connfd = accept(hSocket, NULL, NULL);

    int recvLen = 128;
    char *recvBuf = (char *)malloc(recvLen);
    iRet = SocketRecv(connfd, &recvBuf, &recvLen);
    if (iRet != E_SUCCEED)
    {
        printf("[%s][%s-%lu] Socket recv error.\n", __FILE__, __FUNCTION__, __LINE__);
        close(hSocket);
        exit(0);
    }

    close(hSocket);

    IMAGE_S image;
    iRet = Process(recvBuf, &image);
    if (iRet != E_SUCCEED)
    {
        printf("[%s][%s-%lu] Fail to call process.\n", __FILE__, __FUNCTION__, __LINE__);
    }
    else
    {
        printf("[%s][%s-%lu] Process succeed, [%s](%d).\n", __FILE__, __FUNCTION__, __LINE__, image.imageName, image.imageSize);
        Write(FILENAME_WRITE, image.data, image.imageSize);
    }
    return 0;
}

5.3 Client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include "common.h"
#include "../Base64/base64.h"
#include "../CJSON/cJSON.h"
#include "../Socket/socket.h"
#include "../ReadingAndWriting/readAndwrite.h"

IPADDR_S ipAddr = {"192.168.204.128", 5073};

// 获取忽略掉路径信息的文件名,如 /image/image.png ==> image.png
void GetFileName(const char *filename, char *name)
{
    char *tmp = strstr(filename, "/");
    while (strstr(tmp, "/") != NULL)
    {
        tmp = strstr(tmp, "/");
        tmp++;
    }
    strcpy(name, tmp);
}
char *GetSendBuf(const char *filename)
{
    char *imageData;
    int readLen = Read(filename, &imageData); // 获取原图片及其大小
    if (readLen == 0)
    {
        printf("[%s][%s-%lu] Read error.\n", __FILE__, __FUNCTION__, __LINE__);
        return NULL;
    }

    IMAGE_S stImage;
    GetFileName(filename, stImage.imageName);
    stImage.imageSize = readLen;
    stImage.data = imageData;

    int encodedLen = 0;
    char *encoded = Base64Encode(stImage.data, stImage.imageSize, &encodedLen);

    cJSON *pstRoot = cJSON_CreateObject();
    cJSON_AddStringToObject(pstRoot, "imageName", stImage.imageName);
    cJSON_AddNumberToObject(pstRoot, "imageSize", stImage.imageSize);
    cJSON_AddStringToObject(pstRoot, "dataEncoded", encoded);

    char *pcJson = cJSON_PrintUnformatted(pstRoot);
    int jsonLen = strlen(pcJson);

    int bufLen = jsonLen + HTTP_HDR_LEN;
    char *buf = (char *)malloc(bufLen);
    snprintf(buf, bufLen, CLIENT_HTTP_BUF, ipAddr.ip, jsonLen + 4, pcJson);

    cJSON_Delete(pstRoot);
    SAFE_FREE(stImage.data);
    
    return buf;
}

int main()
{
    int iRet = E_SUCCEED;
    int hSocket;
    int opt = SOCKET_OPT_CONNECT;
    iRet = SocketCreate(&hSocket, opt, ipAddr);
    if (iRet != E_SUCCEED)
    {
        printf("[%s][%s-%lu] Socket create error.\n", __FILE__, __FUNCTION__, __LINE__);
        exit(0);
    }
    
    char *buf = GetSendBuf(FILENAME_READ);
    if (buf == NULL)
    {
        printf("[%s][%s-%lu] Get send buf error.\n", __FILE__, __FUNCTION__, __LINE__);
        close(hSocket);
        exit(0);
    }
    iRet = SocketSend(hSocket, buf, strlen(buf));
    if (iRet == E_SUCCEED)
    {
        printf("[%s][%s-%lu] Socket Send Succeed, SendLen[%d].\n", __FILE__, __FUNCTION__, __LINE__, strlen(buf));
    }
    else if (iRet == E_ERROR)
    {
        printf("[%s][%s-%lu] Socket Send Error.\n", __FILE__, __FUNCTION__, __LINE__);
    }

    close(hSocket);
    return 0;
}

5.4 Tutorial

目录结构:

image-20230326135527840

分别生成 server 和 client 两个可执行文件:

image-20230326135122718

gcc ./Base64/base64.c ./CJSON/cJSON.c ./ReadingAndWriting/readAndwrite.c ./Socket/socket.c ./Main/Server.c -lm -o ./exeFile/Server
gcc ./Base64/base64.c ./CJSON/cJSON.c ./ReadingAndWriting/readAndwrite.c ./Socket/socket.c ./Main/Client.c -lm -o ./exeFile/Client

image-20230326135036166

在两个终端下分别运行 Server 和 Client:

image-20230326135647704

image-20230326135711321

查看图片传输情况:

image-20230326135749156

image-20230326135835587

最后附上源码:
https://melephant.lanzoum.com/irwXt0r4noji

参考资料