Linux 应用编程

Linux Application Programming

<stdio.h>

英语:standard input/output header,标准输入/输出头文件)是C语言为输入输出提供的标准库头文件,其前身是迈克·莱斯克20世纪70年代编写的“可移植输入输出程序库”

类型

标识符说明
size_t用于表示大小的数据类型
FILE记录流的信息的数据结构
fpos_t用于在文件中定位的数据类型

标识符说明
NULL空指针
_IOFBF用于控制缓冲方式的参数
_IOLBF
_IONBF
BUFSIZsetbuf函数使用的缓冲区大小
EOF文件结尾标志
FOPEN_MAX实现保证能够同时打开的文件数量
FILENAME_MAX实现保证支持的最长文件名的字符数
L_tmpnam存放tmpnam函数生成的临时文件名的char数组的大小
SEEK_CUR用于控制文件定位方式的参数
SEEK_END
SEEK_SET
TMP_MAX实现保证能生成的临时文件名个数

标准流

标识符说明fd
stdin标准输入流0
stdout标准输出流1
stderr标准错误流2

函数

类型函数原型
文件操作int remove(const char *filename);
int rename(const char *old, const char *new);
FILE *tmpfile(void);
char *tmpnam(char *s);
int fclose(FILE *stream);
int fflush(FILE *stream);
FILE *fopen(const char * restrict filename, const char * restrict mode);
FILE *freopen(const char * restrict filename, const char * restrict mode, FILE * restrict stream);
void setbuf(FILE * restrict stream, char * restrict buf);
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);
格式化输入输出int fprintf(FILE * restrict stream, const char * restrict format, ...);
int fscanf(FILE * restrict stream, const char * restrict format, ...);
int printf(const char * restrict format, ...);
int scanf(const char * restrict format, ...);
int snprintf(char * restrict s, size_t n, const char * restrict format, ...);
int sprintf(char * restrict s, const char * restrict format, ...);
int sscanf(const char * restrict s, const char * restrict format, ...);
int vfprintf(FILE * restrict stream, const char * restrict format, va_list arg);
int vfscanf(FILE * restrict stream, const char * restrict format, va_list arg);
int vprintf(const char * restrict format, va_list arg);
int vscanf(const char * restrict format, va_list arg);
int vsnprintf(char * restrict s, size_t n, const char * restrict format, va_list arg);
int vsprintf(char * restrict s, const char * restrict format, va_list arg);
int vsscanf(const char * restrict s, const char * restrict format, va_list arg);
字符输入输出int fgetc(FILE *stream);
char *fgets(char * restrict s, int n, FILE * restrict stream);
int fputc(int c, FILE *stream);
int fputs(const char * restrict s, FILE * restrict stream);
int getc(FILE *stream);
int getchar(void);
int putc(int c, FILE *stream);
int putchar(int c);
int puts(const char *s);
int ungetc(int c, FILE *stream);
直接输入输出size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
文件内定位int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
int fseek(FILE *stream, long int offset, int whence);
int fsetpos(FILE *stream, const fpos_t *pos);
long int ftell(FILE *stream);
void rewind(FILE *stream);
错误处理void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
void perror(const char *s);

open()

open()调用既能打开一个业已存在的文件,也能创建并打开一个新文件。

若发生错误,则返回−1,并将 errno 置为相应的错误标志。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 使用这个函数需导入以下头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


// 原型
/*
pathname 要打开的文件由参数 pathname 来标识。如果 pathname 是一符号链接,会对其进行解引用。
如果调用成功,open()将返回一文件描述符,用于在后续函数调用中指代该文件。若发生错误,则返回−1,并将 errno 置为相应的错误标志。

flags 为位掩码,用于指定文件的访问模式。
常用的有如下几个:
O_RDONLY:只读方式打开文件。
O_WRONLY:只写方式打开文件。
O_RDWR:读写方式打开文件。
O_CREAT:如果文件不存在,则创建文件。
O_EXCL:与O_CREAT一起使用,如果文件已存在,则返回错误。
O_TRUNC:如果文件已存在,将其截断为0字节。
O_APPEND:在文件末尾追加写入。
O_NONBLOCK:以非阻塞方式打开文件。

mode 参数仅在创建新文件使用,当调用 open()创建新文件时 mode 指定了文件的访问权限。
S_IRUSR:用户可读权限。
S_IWUSR:用户可写权限。
S_IXUSR:用户可执行权限。
S_IRGRP:组可读权限。
S_IWGRP:组可写权限。
S_IXGRP:组可执行权限。
S_IROTH:其他人可读权限。
S_IWOTH:其他人可写权限。
S_IXOTH:其他人可执行权限。

return 调用所返回的文件描述符数值
调用成功返回一个大于0的数字,这个数字是文件描述符。(后续对打开或创建的文件,进行读或写的操作时候都需要用到这个文件描述符)。调用失败返回-1。
SUSv3 规定,如果调用 open()成功,必须保证其返回值为进程未用文件描述符中数值最小者(优先用数值较小的文件描述符)。
*/
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

open 函数

open() 是一个系统调用函数,用于在 Linux 系统中打开或者创建一个文件。

1
2
3
4
5
6
#include <sys/types.h>  // 使用这个函数需导入以下头文件
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

fopen函数

fopen() 函数是标准 C 库中用来打开文件的函数。

1
2
#include <stdio.h> // 使用此函数需导入此头文件
FILE *fopen(const char *filename, const char *mode);

fclose()

关闭流 stream。刷新所有的缓冲区(相当于做了fflush)。

fclose函数的作用

  • 清空相关的缓冲区

    以写文件为例,程序会先将文本信息存入缓冲区中,然后根据缓冲区刷新指令或者等缓冲区满了以后才向文件写入,那么我们如果不使用fclose()关闭流,就有可能有部分文本信息在缓冲区中没有写入到文本文件中,从而导致文本信息缺失.但是这东西看运气.

  • 释放内存

    我们知道,如果我们不手动关闭这个流,系统会维持打开的状态,维持当然需要消耗一定的内存资源,本着苍蝇再小也是块肉的原则当然是要把它关掉了,其实如果有很多流在使用后没有关闭,这部分内存资源也是十分可观的.

多次fclose()会发生什么

能不能多次对同一个文件描述符调用fclose?

答案是不行的

为啥不行,我们需要看看fclose到底干了什么

fclose关闭的是一个文件流,当调用fclose时,将会使用户态(C库)缓冲区中的数据刷新到内核区域(或通过socket发送,或者仅仅写回硬盘)。
于此同时,fclose会调用free来释放fp所指向的用户态buffer。

因此,如果多次调用fclose,会多次调用free来释放一块已经被释放的区域,而这个动作是危险的。
很多解释是说,因为一旦被释放之后,这块区域就属于未知区域。有可能系统已经把该区域分给了其他进程,所以不能多次fclose。

但是我看过Linux关于close的内核源码,发现并不是这样,当第一次调用close时,将会把对应的文件描述符数组flip[fp] = NULL。如果再调用close时,会在前面判断该项是否为null,如果为null,return -EINVAL;因此,多次调用fclose时会报错,但不会影响系统其他进程。

来源CSDN

*调用fclose之后,FILE fp会变成NULL吗?

不会,没有什么机制把fp置空的动作,只是这个时候fp所指向的区域已经不再有效。

来源CSDN

fflush()

刷新流 stream 的输出缓冲区,将数据刷新到内核缓冲。

描述

C 库函数 int fflush(FILE *stream) 刷新流 stream 的输出缓冲区。

声明

下面是 fflush() 函数的声明。

1
int fflush(FILE *stream)

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个缓冲流。

返回值

如果成功,该函数返回零值。如果发生错误,则返回 EOF,且设置错误标识符(即 feof)。

fsync()

fsync函数同步内存中所有已修改的文件数据到储存设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 头文件
#include <unistd.h>

// 原型
int fsync(int fd);

// 参数
fd 是该进程打开来的文件描述符

// 返回值
函数成功执行时,返回0。失败返回-1,errno被设为以下的某个值
EBADF:文件描述词无效
EIO:读写的过程中发生错误
EROFS, EINVAL:文件所在的文件系统不支持同步
调用 fsync 可以保证文件的修改时间也被更新。fsync 系统调用可以使您精确的强制每次写入都被更新到磁盘中。您也可以使用同步(synchronous)I/O 操作打开一个文件,这将引起所有写数据都立刻被提交到磁盘中。通过在 open 中指定 O_SYNC 标志启用同步I/O。

perror()

errno是一个错误编号,本质上是一个整形的变量,每个错误对应着一个错误编号,当你调用”某些“函数出错时,这个函数会更新errno的值。

也就是说,并不是所有的函数都会将错误编号输出到errno变量中。

描述

C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。

声明

下面是 perror() 函数的声明。

1
void perror(const char *str)

参数

  • str – 这是 C 字符串,包含了一个自定义消息,将显示在原本的错误消息之前。

<stdlib.h>

常量

stdlib.h中定义的常量:

名字描述
NULL一般定义为0, 或0L, 或(void*)0表示空指针常量的; 换句话说,一个常量用来表示一个总是指向无效的内存地址的指针值。
EXIT_FAILURE一个非0用来指示程序失败的结束,一般用于exit().
EXIT_SUCCESS0用来指示程序成功的结束,一般用于exit().
RAND_MAX>= 32767函数rand()所能返回的最大的值.
MB_CUR_MAX当前locale中多字节字符的最大字节数目

数据类型

stdlib.h中定义的数据类型

名字描述
size_t算子sizeof返回结果的数据类型,实际上是无符号整型.
div_tldiv_tlldiv_t函数div, ldiv, lldiv的返回结果的数据类型,实际上是包含两个整数的结构类型.

函数

stdlib.h中声明的库函数可分为六类:类型转换、伪随机数、动态内存分配与回收管理、进程控制、搜索及排序、简单数学。

名字描述
类型转换
atof字符串转换为双精度浮点数。相当于strtod(s, (char**)NULL).
atoi把字符串转换为整型. 相当于(int)strtol(s, (char**)NULL, 10).
atol把字符串转换为长整型. Equivalente a strtol(s, (char**)NULL, 10).
atoll把字符串转换为长长整型. Equivalente a strtol(s, (char**)NULL, 10). 这是C99新增加的库函数。
strtod把字符串转换为双精度浮点数,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtof把字符串转换为单精度浮点数,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtold把字符串转换为长双精度浮点数,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtol把字符串转换为长整型,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtoll把字符串转换为long long int,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtoul把字符串转换为无符号长整形,检查结果是否溢出,并返回字符串不能转换部分的地址.
strtoull把字符串转换为unsigned long long int,检查结果是否溢出,并返回字符串不能转换部分的地址.
伪随机数序列生成
rand返回在0到RAND_MAX之间的伪随机数. 不接受参数作为随机数种子,因此产生的伪随机数列相同,有利于程序调试。
srand初始化rand()接受无符号整型参数作为伪随机数种子.如果种子相同,伪随机数列也相同。
内存的分配与释放
aligned_alloc边界对齐的动态内存分配.
calloc数组的动态内存分配,且初始化为全零
malloc动态内存分配,其内容不初始化
realloc释放老的动态内存块,按照给出的尺寸分配新的动态内存块,老的内存块的内容尽量复制到新的内存块
free系统释放动态分配的内存. 如果是空指针,则无动作发生;如果指针所指不是动态分配的内存块或者是已释放的内存块,则行为是未定义的。
进程控制/与运行环境的沟通
abort导致程序非正常的结束,各种流缓冲区与临时文件直接放弃。实际上抛出raise(SIGABRT),缺省的信号处理行为是使用退出代码3执行终止(terminate)操作。如果SIGABRT被捕捉且信号处理程序不返回,则程序将不终止.
atexit登记一个函数,当程序使用exit正常退出时被登记的函数自动被调用.
exit程序正常终止。首先atexit()登记的函数按照登记的逆序被调用;如果多次调用atexit登记了多个函数,按照登记的逆序调用这些函数。如果一个函数被登记了多次,则程序正常退出时该函数也将被调用多次。然后所有缓冲区中的数据被写回(flushed);所有打开着的流被关闭;tmpfile函数创建的文件被删除。最后,控制权返回给调用环境,返回数值表示程序返回时的状态,0表示EXIT_SUCCESS, 1表示EXIT_FAILURE.
at_quick_exit登记一个函数,当程序使用quick_exit正常退出时被登记的函数自动被调用.
_Exit程序正常终止, 但atexit(), at_quick_exit(), signal()登记的函数不被调用; 打开的流、文件是否被关闭,由编译器的实现者决定
getenv获得某一个环境变量的字符串值,如果该环境变量不存在,返回NULL.
quick_exit程序正常终止, 但atexit(), 登记的函数不被调用; at_quick_exit()登记的函数按登记顺序的逆序被调用。
system把参数作为外部环境的命令执行。 如果参数为空,则判断外部环境是否有命令解释器。
搜索与排序
bsearch折半搜索.
qsort快速排序.
整数算术
abs, labs, llabs计算整数的绝对值.
div, ldiv, lldiv计算整数除法的商与余数.
多字节字符/宽字符转换
mblen计算多字节字符的长度并确定是否为有效字符 .
mbtowc多字节字符转换为宽字符.
wctomb宽字符转换为多字节字符.
多字节字符串/宽字符串转换
mbstowcs多字节字符串转换为宽字符串.
wcstombs宽字符串转换为多字节字符串.

atof() str 转 double

1
double atof(const char *nptr);

函数说明

atof()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(‘\0’)才结束转换,并将结果返回。参数nptr字符串可包含正负号、小数点或E(e)来表示指数部分,如123.456或123e-2。

相关函数

  • strtod(nptr,(char**)NULL)
  • gcvt 浮点型数转换为字符串,取四舍五入

atoi() str 转 int

1
int atoi(const char *str)

将字符串参数str转换为整数(int 类型)。

在字符串的开头,atoi() 方法忽略所有空格,转换紧跟在空格之后的单词,然后在遇到第一个非数字单词时停止。atoi() 模块实现了字符串的整数描述。

参数

  • str – 这是整数的字符串表示形式。

返回值

此函数将转换后的整数作为 int 值返回。如果无法执行有效的转换,则返回零。

实例

1
val = atoi("123456");

atoi 输入参数少,使用简单,在一些明确知道数值范围的情况下使用比较合适,转换范围相较于strtol(64位环境下)比较小,而且也不知道返回值为0时是转换失败还是转换成功。

strtol 输入参数相对较多,用法也更灵活支持转换成不同进制,能够明确的知道输入数据转换出错的位置和错误原因,相比较与atoi,对于0这种特殊的字符串,能更清楚的知道是转换成功的得到的还是转换失败得到的。

综上,strtol使用更灵活,功能更强大,且可以判断转换结果的正确性,建议字符串转换为整型时使用strtol

atol() str 转 long int

gcvt() double 转 str

1
2
3
4
5
6
7
8
9
char *gcvt(double number,size_t ndigits,char *buf);

/*
函数说明
gcvt()用来将参数number转换成ASCII码字符串,参数ndigits表示显示的位数。gcvt()与ecvt()和fcvt()不同的地方在于,gcvt()所转换后的字符串包含小数点或正负符号。若转换成功,转换后的字符串会放在参数buf指针所指的空间。

返回值
返回一字符串指针,此地址即为buf指针。
*/

strtod() str 转 double

1
double strtod(const char *nptr,char **endptr);

strtol() str 转long int 不同进制

C 库函数 strtol() 把参数 str 所指向的字符串根据给定的 base 转换为一个长整数(类型为 long int 型),base 必须介于 2 和 36(包含)之间,或者是特殊值 0。

strtol() 方法将具有字符串数据类型的值转换为长整数。strtol() 方法跳过字符串开头的所有空白字符,将连续的字符转换为部分的一部分,然后在到达第一个非数字字符时终止。

strtol() 函数对异常的返回可以设置errno,从而可以发现异常的返回。

声明

1
long int strtol(const char *str, char **endptr, int base)

参数

  • str – 要转换为长整数的字符串。
  • endptr – 对类型为 char* 的对象的引用,其值由函数设置为 str 中数值后的下一个字符。
  • base – 基数,必须介于 2 和 36(包含)之间,或者是特殊值 0。

返回值

该函数返回转换后的长整数,如果没有执行有效的转换,则返回一个零值。

实例

下面的实例演示了 strtol() 函数的用法。

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
26
27
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
char str[30] = "2030300 This is test";
char *ptr;
long ret;

errno = 0;
ret = strtol(str, &ptr, 10);
/* Check for various possible errors */
if ((errno == ERANGE && (ret == LONG_MAX || ret == LONG_MIN)) || (errno != 0 && ret == 0)) {
perror("strtol");
exit(EXIT_FAILURE);
}
printf("数字(无符号长整数)是 %ld\n", ret);
printf("字符串部分是 |%s|", ptr);

return(0);
}
/*
输出:
数字(无符号长整数)是 2030300
字符串部分是 | This is test|
*/

strtoul() str 转 unsigned long int

toascii() int 转 ascii

1
2
3
4
5
6
7
8
9
10
int toascii(int c)

/*
函数说明
toascii()会将参数c转换成7位的unsigned char值,第八位则会被清除,此字符即会被转成ASCII码字符。

返回值
将转换成功的ASCII码字符值返回。

*/

tolower() 大写 转 小写

toupper() 小写 转 大写

strtok() 用分隔符分解字符串

描述

C 库函数 strtok() 分解字符串 str 为一组字符串,delim 为分隔符。

声明

下面是 strtok() 函数的声明。

1
char *strtok(char *str, const char *delim)

参数

  • str – 要被分解成一组小字符串的字符串。输入NULL则是从上次结束的位置继续查找。
  • delim – 包含分隔符的 C 字符串。

返回值

该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。

实例

1
2
3
4
5
6
7
8
char str[] = "8.8.8.8"
char s[] = "."
char *token;
token = strtok(str, s);
while (token != NULL)
{
token = strtok(NULL, s);
}

让我们编译并运行上面的程序,这将产生以下结果:

1
2
3
This is 
www.runoob.com
website

<netinet/in.h>

网络中的数据传输是按 大端 数据格式来传输的。

所以在一些设备上需要进行大小端转换,也就是所谓的网络字节序转换

htonl() htons() ntohl() ntohs()

1
2
3
4
5
6
7
8
9
// host to network long
uint32_t htonl(uint32_t hostlong);
// host to network short
uint16_t htons(uint16_t hostshort);

// network to host long
uint32_t ntohl(uint32_t netlong);
// network to host short
uint16_t ntohs(uint16_t netshort);

INET_ADDRSTRLEN IPv4 str len

1
2
3
// 预定义的字符串长度
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

<arpa/inet.h>

inet_addr() str 转 in_addr

1
in_addr_t inet_addr(const char *);

如果参数char *cp无效,函数返回-1(INADDR_NONE),

这个函数在处理地址为255.255.255.255时也返回-1,

255.255.255.255是一个有效的地址,不过inet_addr无法处理;

建议使用 inet_pton() 替代此函数

inet_aton() str 转 in_addr

inet_ntoa是一个用于将IPv4地址的二进制值转换为点分十进制格式的函数。它的函数原型如下:

1
int inet_aton(const char *cp, struct in_addr *inp);

其中,cp是一个指向以点分十进制格式表示的IPv4地址的字符串,inp是一个指向in_addr结构体的指针,该结构体包含了转换后的二进制值。

如果转换成功,inet_aton返回非0值,否则返回0值。需要注意的是,如果提供的字符串地址不合法,那么inet_aton返回0值,而不会设置errno

inet_aton已经被弃用,推荐使用inet_pton函数代替。

inet_ntoa() in_addr 转 str

inet_ntoa是一个用于将IPv4地址的二进制值转换为点分十进制格式的函数。它的函数原型如下:

1
char *inet_ntoa(struct in_addr);

其中,in是一个指向in_addr结构体的指针,该结构体包含了IPv4地址的二进制值。

inet_ntoa返回一个指向以点分十进制格式表示的IPv4地址的字符串,如果转换失败,则返回NULL

inet_ntoa已经被弃用,推荐使用inet_ntop函数代替。

inet_pton() str 转 in_addr

1
int inet_pton(int af, const char *restrict src , void *restrict dst );

函数中的p和n分别代表表达式(presentation)数值(numeric)

这里的表达式一般是指字符串,数值对于IPv4就是uint32

inet_pton() 转换网络主机地址表达式(presentation)为二进制数值(numeric),支持IPv4 to IPv6,线程安全,可重入。

参数

  • af – 仅支持AF_INETAF_INET6,IPv4 IPv6
  • src – 表达式(presentation)字符串
  • dst – 数值(numeric)指针

返回值

inet_pton () 成功返回1(网络地址转换成功)。如果src不包含表示指定地址族中有效网络地址的字符串,则返回 0 。如果af不包含有效地址族,则返回 -1 并将errno设置为EAFNOSUPPORT。

示例

1
2
3
4
5
6
7
8
9
10
11
12
char addr_str[] = "8.8.8.8";
struct in_addr addr;

if((res = inet_pton(AF_INET, addr_str, &addr)) <= 0) {
// inet_pton error
if(res == 0) {
// Invalid expression
} else if (res < 0 ) {
// error
}
}
// success

inet_ntop() in_addr 转 str

1
2
3
4
const char *inet_ntop(int af, 
const void *restrict src,
char *restrict dst,
socklen_t size);

inet_ntop() 函数转换二进制数值(numeric)为网络主机地址表达式(presentation),支持IPv4 to IPv6,线程安全,可重入。

参数

  • af – 仅支持AF_INETAF_INET6,IPv4 or IPv6
  • src – 数值(numeric)指针
  • dst – 表达式(presentation)字符串
  • size – 此缓冲区可用字节数

返回值

成功时,inet_ntop() 返回一个指向 dst非空指针。如果有错误则返回NULL,errno设置为指示错误。

示例

1
2
3
4
5
6
7
char addr_str[16];
struct in_addr addr;

if( inet_ntop(AF_INET, &addr, addr_str, sizeof(addr_str)) == NULL ) {
// inet_ntop error
}
// success
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
26
27
28
29
30
31
32
33
34
35
36
// man 手册
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
unsigned char buf[sizeof(struct in6_addr)];
int domain, s;
char str[INET6_ADDRSTRLEN];

if (argc != 3) {
fprintf(stderr, "Usage: %s {i4|i6|<num>} string\n", argv[0]);
exit(EXIT_FAILURE);
}

domain = (strcmp(argv[1], "i4") == 0) ? AF_INET : (strcmp(argv[1], "i6") == 0) ? AF_INET6 : atoi(argv[1]);

s = inet_pton(domain, argv[2], buf);
if (s <= 0) {
if (s == 0)
fprintf(stderr, "Not in presentation format");
else
perror("inet_pton");
exit(EXIT_FAILURE);
}

if (inet_ntop(domain, buf, str, INET6_ADDRSTRLEN) == NULL) {
perror("inet_ntop");
exit(EXIT_FAILURE);
}

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

exit(EXIT_SUCCESS);
}

Socket

socket 是一种 IPC 方法,它允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据。

概述

使用 socket()系统调用能够创建一个 socket,它返回一个用来在后续系统调用中引用该 socket 的文件描述符

1
2
3
4
int socket (int __domain, int __type, int __protocol);

// 在绝大部分情况下 protocol=0
fd = socket(domain, type, protocol)

socket domain
socket 存在于一个通信 domain 中,它确定:

  • 识别出一个 socket 的方法(即 socket“地址”的格式);
  • 通信范围(即是在位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间)。

现代操作系统至少支持下列 domain

  • UNIX (AF_UNIX) domain 允许在同一主机上的应用程序之间进行通信。
  • IPv4 (AF_INET) domain 允许在使用因特网协议第 4 版(IPv4)网络连接起来的主机上的应用程序之间进行通信。
  • IPv6 (AF_INET6) domain 允许在使用因特网协议第 6 版(IPv6)网络连接起来的主机上的应用程序之间进行通信。
Domain执行的通信应用程序间的通信地址格式
AF_UNIX内核中同一主机路径名
AF_INET通过 IPv4通过 IPv4 网络连接起来的主机32 位 IPv4 地址+16 位端口号
AF_INET6通过 IPv6通过 IPv6 网络连接起来的主机128 位 IPv6 地址+16 位端口号

socket type
每个 socket 实现都至少提供了两种 socket:STREAM (流)和 DGRAM (数据报)。这两种 socket 类型在 UNIX 和 Internet domain 中都得到了支持。

属性socket 类型socket 类型
STREAMDGRAM
可靠地递送?yesno
面向连接?yesno
消息边界保留?noyes
  1. SOCK_STREAM (流)

    流 socket(SOCK_STREAM)提供了一个可靠的双向的字节流通信信道。

    • 可靠的:表示可以保证发送者传输的数据会完整无缺地到达接收应用程序(假设网络链接和接收者都不会崩溃)或收到一个传输失败的通知。
    • 双向的:表示数据可以在两个 socket 之间的任意方向上传输。
    • 字节流:表示与管道一样不存在消息边界的概念(参见 44.1 节)。

    一个流 socket 类似于使用一对允许在两个应用程序之间进行双向通信的管道,它们之间
    的差别在于(Internet domain)socket 允许在网络上进行通信。

    流 socket 的正常工作需要一对相互连接的 socket,因此流 socket 通常被称为面向连接的。

  2. SOCK_DGRAM (数据报)

    允许数据以被称为数据报的消息的形式进行交换。在数据报 socket 中,消息边界得到了保留,但数据传输是不可靠的消息的到达可能是无序的、重复的或者根本就无法到达

    数据报 socket 是更一般的无连接 socket 概念的一个示例。与流 socket 不同,一个数据报socket 在使用时无需与另一个 socket 连接。(在 56.6.2 节中将会看到数据报 socket 可以与另一个 socket 连接,但其语义与连接的流 socket 是不同的。)

在 Internet domain 中,数据报 socket 使用了用户数据报协议(UDP),而流 socket 则(通常)使用了传输控制协议(TCP)

一般来讲,在称呼这两种 socket 时不会使用术语“Internet domain 数据报 socket”和“Internet domain 流 socket”,而是分别使用术语“UDP socket”和“TCP socket”。

socket 系统调用
关键的 socket 系统调用包括以下几种。

  • **socket()**系统调用
    • 创建一个新 socket。
  • **bind()**系统调用
    • 将一个 socket 绑定到一个地址上。
    • 通常,服务器需要使用这个调用来将其 socket 绑定到一个众所周知的地址上使得客户端能够定位到该 socket 上。
  • **listen()**系统调用
    • 允许一个流 socket 接受来自其他 socket 的接入连接。
  • **accept()**系统调用
    • 在一个监听流 socket 上接受来自一个对等应用程序的连接,并可选地返回对等 socket 的地址。
  • **connect()**系统调用
    • 建立与另一个 socket 之间的连接。

socket I/O 可以使用传统的 read()和 write()系统调用或使用一组 socket 特有的系统调用(如send()、recv()、sendto()以及 recvfrom())来完成。

在默认情况下,这些系统调用在 I/O 操作无法被立即完成时会阻塞。通过使用 fcntl() F_SETFL 操作(5.3 节)来启用 O_NONBLOCK 打开文件状态标记可以执行非阻塞 I/O。

Note

Misc.

assert() 断言

assert()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句,它的作用是终止程序以免导致严重后果,同时也便于查找错误

所需头文件:#include <assert.h>

报告错误的条件:

1
2
3
void assert(
int expression
);

参数:Expression (including pointers) that evaluates to nonzero or 0.(表达式【包括指针】是非零或零)

原理:assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
示例程序:

1
2
3
4
5
6

void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言
//...
}

总结

  • 在函数开始处检验传入参数的合法性

  • 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败

  • 不能使用改变环境的语句,因为assert只在DEBUG个生效,如果这么做,会使用程序在真正运行时遇到问题

  • assert和后面的语句应空一行,以形成逻辑和视觉上的一致感

  • 有的地方,assert不能代替条件过滤

    ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略掉。

何时需要使用断言

  • 可以在预计正常情况下程序不会到达的地方放置断言:ASSERT( FALSE );
  • 断言可以用于检查传递给私有方法的参数。(对于公有方法,因为是提供给外部的接口,所以必须在方法中有相应的参数检验才能保证代码的健壮性)
  • 使用断言测试方法执行的前置条件和后置条件
  • 使用断言检查类的不变状态,确保任何情况下,某个变量的状态必须满足。(如age属性应大于0小于某个合适值)

什么地方不要使用断言

断言语句不是永远会执行,可以屏蔽也可以启用

  • 不要使用断言作为公共方法的参数检查,公共方法的参数合法性永远都要执行
  • 断言语句不可以有任何边界效应,不要使用断言语句去修改变量和改变方法的返回值

Note

sync、fsync、fdatasync、fflush对比

c库缓冲 —–fflush—–> 内核缓冲 —–fsync—–> 磁盘

  1. void sync(void) 函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。

  2. int fsync(int fd) 函数是系统提供的系统调用。只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。fsync还会同步更新文件的属性,即inode部分。

  3. int fdatasync(int fd) 函数类似于fsync,但它只影响文件的数据部分。它并不会更新文件的属性

  4. int fflush(FILE *stream):标准I/O函数(如:fread,fwrite)会在内存建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync(先调用fflush后调用fsync,否则不起作用)。fflush接受一个参数FILE *。它还可以刷新标准输入输出。fclose函数在关闭文件之前也会做flush操作

关于缓冲.

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。

  • 全缓冲:如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。

  • 行缓冲:如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。

  • 无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

虽然write系统调用位于C标准库I/O缓冲区的底层,被称为Unbuffered I/O函数,但在write的底层也可以分配一个内核I/O缓冲区,所以write 也不一定是直接写到文件的,也可能写到内核I/O缓冲区中。

可以使用fsync函数同步至磁盘文件,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的,而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的。


Linux 应用编程
https://www.oikiou.top/2023/47a3870e/
作者
Oikiou
发布于
2023年3月26日
许可协议