一.网络常用接口
Linux协议栈与posix api的关系 背景
众所周知,Linux内核协议栈是Linux内核的网络管理模块的具象化表达实例,用户空间如果想使用Linux内核提供的网络服务就需要使用内核提供的一系列网络相关的posix接口(操作系统原语,或者说原生接口)。
以TCP通信举例,我们想开发一套基于C/S通信架构的服务,需要获取一个tcp的socket,该socket本质上是一个文件描述符,也可以说是一个内核中tcb(tcp control block)的映射。当我们在应用层调用connect,send等posix相关的api(系统调用)时,内核就会操作tcb的传输内存,按照tcp协议的标准,进行握手建立连接并发送用户数据。
网络开发必用结构体
通过struct sockaddr_in结构体指定协议族,指定绑定地址,指定监控的端口号。
使用的成员:sin_family、sin_addr.s_addr、sin_port;
1.1 socket
socket:
#include<sys/types.h> #include<sys/socket.h> int socket(int domain, int type, int protocol);
这个函数建立一个协议族、协议类型、协议编号的socket文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
domain参数值含义:
名称 含义
PF_UNIX,PF_LOCAL 本地通信
AF_INET,PF_INET IPv4协议
PF_INET6 IPv6协议
PF_NETLINK 内核用户界面设备
PF_PACKET 底层包访问
type参数值含义:
名称 含义
SOCK_STREAM TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM UDP连接
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
:SOCK_PACKET 专用类型
SOCK_RDM 提供可靠的数据报文,不保证数据有序
SOCK_RAW 提供原始网络协议访问
protocol参数含义:
通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;如果协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
1.8 close
该接口用于关闭套接字,当应用程序调用close函数关闭套接字时,内核会释放该套接字所占用的资源,并将其从内核数据结构中移除。
接口也很简单,直接close(sockfd)即可。
服务端posix接口
bind
#include<sys/types.h> #include<sys/socket.h> int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第2个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
第3个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败
1.3 listen
listen
#include<sys/socket.h> int listen(int sockfd, int backlog); @sockfd 创建的socket 返回的文件描述符 @return 返回值:若成功,返回 0;若出错,返回-1
参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第2个参数规定了内核应该为相应套接字排队的最大连接个数。
分析
服务端调用listen宣告它接受连接请求,其参数backlog提供了一个提示,提示该进程所要入队的未完成连接的请求数量。为什么说是提示呢,因为根据Linux内核的不断迭代,实际上该socket可以容纳的未连接请求远远不止传入的这个数目,其数量可以自适应增加,但不会超过其上限:<sys/socket.h>中的SOMAXCONN;
1.4 accept
服务端调用了listen让主动套接字变被动监听套接字,就可以使用accept函数获得连接请求并建立连接;
accept
#include<sys/types.h> #include<sys/socket.h> int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); @sockfd:服务端socket,该socket仅用于接收连接请求,获取的新socket的fd与之无关。 @addr:一个指向struct sockaddr类型变量的指针,用于存储客户端地址信息。 @len:一个指向socklen_t类型变量的指针,表示addr结构体长度。 @return 返回值:若成功,返回文件 (套接字) 描述符;若出错,返回-1
参数说明:
sockefd:套接字描述符,该套接字在listen()后监听连接。
addr:(可选)指针。组客户端结构体 指向一个缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针。输入参数,配合addr一起使用,指向存有addr地址长度的整形数。
分析:
当服务器程序调用accept()时,如果此时有客户端发送了连接请求,那么它将立即从内核中已经建立的全连接队列中,寻找该请求的五元组信息。如果确认该客户端已经处于全连接队列,那么就返回新分配给该客户端所用的套接字文件描述符。
此时服务器进程就可以利用该文件描述符与该客户进程进行通信了。同时,在accept()函数中会填充传入参数中对应结构体(如addr)的内容,以获得远程主机地址信息等相关信息。
注意
- 当没有连接请求到达时,accept()函数会一直阻塞等待,并且只有在监听套接字上有连接请求时,它才会返回新的套接字描述符,可以将socket设置为非阻塞从而马上获得获取结果。(此时服务器状态已经编程被动)
- 由于accept()函数是阻塞式函数,因此可能会导致服务器程序被挂起,为了避免这种情况的出现,可以通过使用多线程或者多路复用等技术来实现同时处理多个客户端请求。
- accept()函数只能用于监听TCP协议的连接请求,不能用于UDP协议。
1.5 connect
connect
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
sockfd:向套接字中发送数据
buf:要发送的数据的首地址
len:要发送的数据的字节
int flags:设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样
返回值:成功返回实际发送的字节数,失败返回 -1
1.7 recv
#include<sys/types.h> #include<sys/socket.h> int recv( int fd, char *buf, int len, int flags);
参数说明:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
在非阻塞状态下,recv 返回值处理!
在非阻塞状态下,recv() 接口在被调用后立即返回,返回值代表了不同的含义。如在本例中,
- recv() 返回值大于0,表示接受数据完毕,返回值即是接受到的字节数;
- recv() 返回0,表示连接已经正常断开;
- recv() 返回-1,且errno 等于EAGAIN,表示recv 操作还没执行完成;
- recv() 返回-1,且errno 不等于EAGAIN,表示recv 操作遇到系统错误errno。
总结
网络服务端和客户端常用的Posix api;
服务端 | 客户端 |
socket() | socket() |
bind() | bind()可选 |
listen() | connect() |
accetp() | |
send() | send() |
recv() | recv() |
二. 修改句柄属性
fctl 函数作用:功能描述:根据文件描述词来操作文件的特性。
网络IO默认的连接是阻塞方式的,可以使用fcntl函数进行设置非阻塞模式。
函数原型:
#include<unistd.h> #include<fcntl.h> int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd ,struct flock* lock); // 返回值:成功依赖cmd的值,失败返回-1;
fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.
针对cmd的值,fcntl能够接受第三个参数(arg)
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
cmd参数说明:
参数 含义
F_GETFL 获取文件状态标志
F_SETFL 设置文件状态标志
F_GETFD 获取文件描述符标志
F_SETFD 设置文件描述符标志
F_GETLK 获取文件锁
F_SETLK 设置文件锁
F_DUPFD 复制文件描述符
F_GETOWN 取当前接受SIGIO和SIGURG信号的进程ID和进程组ID.正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程中ID
F_SETOWN 设置当前接受SIGIO和SIGURG信号的进程ID和进程组ID.
状态标志:
标志 含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 为读、写打开
O_APPEND 每次写时追加
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(数据)
O_RSYNC 同步读、写
O_FSYNC 等待写完成(进FreeBSD和Mac OS X)
O_ASYNC 异步I/O(进FreeBSD和Mac OS X)
cmd 详解
F_DUPFD
返回一个如下描述的(文件)描述符:
1. 最小的大于或等于arg的一个可用的描述符与原始操作符一样的某对象的引用
2. 如果对象是文件(file)的话,返回一个新的描述符,这个描述符与arg共享相同的偏移量(offset)
3. 相同的访问模式(读,写或读/写)
4. 相同的文件状态标志(如:两个文件描述符共享相同的状态标志)
5. 与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用
F_GETFD
取得与文件描述符fd联合close-on-exec标志,类似FD_CLOEXEC.如果返回值和FD_CLOEXEC进行与运算结果是0的话,文件保持交叉式访问exec(), 否则如果通过exec运行的话,文件将被关闭(arg被忽略)
F_SETFD 设置close-on-exec旗标。该旗标以参数arg的FD_CLOEXEC位决定。
F_GETFL 取得fd的文件状态标志,如同下面的描述一样(arg被忽略)
F_SETFL 设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。
F_GETOWN 取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)
F_SETOWN 设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程组id通过提供负值的arg来说明,否则,arg将被认为是进程id
O_NONBLOCK 非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误
O_APPEND 强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志
O_DIRECT 最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据. 如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能
O_ASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候
注意:
非阻塞要在accpt函数之前设置才能生效。
在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
fcntl的返回值:
与命令有关。如果出错,所有命令都返回-1,
如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
2.2 特殊作用
2.2.1 设置阻塞非阻塞
借助F_SETFD或F_SETFL命令实现对于fd 修改;
2.2.1 实例
/**********************使能非阻塞I/O********************/ int flags; if(flags = fcntl(fd, F_GETFL, 0) < 0) { perror("fcntl"); return -1; } flags |= O_NONBLOCK; if(fcntl(fd, F_SETFL, flags) < 0) { perror("fcntl"); return -1; } / *******************************************************/ /**********************关闭非阻塞I/O******************/ flags &= ~O_NONBLOCK; if(fcntl(fd, F_SETFL, flags) < 0) { perror("fcntl"); return -1; } / *******************************************************/
2.2.2设置获取记录锁
结构体flock的指针:
struct flcok { short int l_type; /* 锁定的状态*/ //这三个参数用于分段对文件加锁,若对整个文件加锁,则:l_whence=SEEK_SET,l_start=0,l_len=0; short int l_whence;/*决定l_start位置*/ off_t l_start; /*锁定区域的开头位置*/ off_t l_len; /*锁定区域的大小*/ pid_t l_pid; /*锁定动作的进程*/ };
l_type 有三种状态:
F_RDLCK 建立一个供读取用的锁定
F_WRLCK 建立一个供写入用的锁定
F_UNLCK 删除之前建立的锁定
l_whence 也有三种方式:
SEEK_SET 以文件开头为锁定的起始位置。
SEEK_CUR 以目前文件读写位置为锁定的起始位置
SEEK_END 以文件结尾为锁定的起始位置。
2.2.2 实例
1 #include "filelock.h" 2 3 /* 设置一把读锁 */ 4 int readLock(int fd, short start, short whence, short len) 5 { 6 struct flock lock; 7 lock.l_type = F_RDLCK; 8 lock.l_start = start; 9 lock.l_whence = whence;//SEEK_CUR,SEEK_SET,SEEK_END 10 lock.l_len = len; 11 lock.l_pid = getpid(); 12 // 阻塞方式加锁 13 if(fcntl(fd, F_SETLKW, &lock) == 0) 14 return 1; 15 16 return 0; 17 } 18 19 /* 设置一把读锁 , 不等待 */ 20 int readLocknw(int fd, short start, short whence, short len) 21 { 22 struct flock lock; 23 lock.l_type = F_RDLCK; 24 lock.l_start = start; 25 lock.l_whence = whence;//SEEK_CUR,SEEK_SET,SEEK_END 26 lock.l_len = len; 27 lock.l_pid = getpid(); 28 // 非阻塞方式加锁 29 if(fcntl(fd, F_SETLK, &lock) == 0) 30 return 1; 31 32 return 0; 33 } 34 /* 设置一把写锁 */ 35 int writeLock(int fd, short start, short whence, short len) 36 { 37 struct flock lock; 38 lock.l_type = F_WRLCK; 39 lock.l_start = start; 40 lock.l_whence = whence; 41 lock.l_len = len; 42 lock.l_pid = getpid(); 43 44 //阻塞方式加锁 45 if(fcntl(fd, F_SETLKW, &lock) == 0) 46 return 1; 47 48 return 0; 49 } 50 51 /* 设置一把写锁 */ 52 int writeLocknw(int fd, short start, short whence, short len) 53 { 54 struct flock lock; 55 lock.l_type = F_WRLCK; 56 lock.l_start = start; 57 lock.l_whence = whence; 58 lock.l_len = len; 59 lock.l_pid = getpid(); 60 61 //非阻塞方式加锁 62 if(fcntl(fd, F_SETLK, &lock) == 0) 63 return 1; 64 65 return 0; 66 } 67 68 /* 解锁 */ 69 int unlock(int fd, short start, short whence, short len) 70 { 71 struct flock lock; 72 lock.l_type = F_UNLCK; 73 lock.l_start = start; 74 lock.l_whence = whence; 75 lock.l_len = len; 76 lock.l_pid = getpid(); 77 78 if(fcntl(fd, F_SETLKW, &lock) == 0) 79 return 1; 80 81 return 0; 82 }
三、文件操作函数
fwrite() 函数
用于将数据块按字节写入到文件中,返回实际成功写入的数据块个数。
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
ptr:指向要写入的数据块的指针。
size:每个数据块的字节数。
count:要写入的数据块个数。
stream:指向要写入的文件的指针。
使用 fwrite() 函数时,它会将 size 字节的数据块从 ptr 写入到指定的文件流 stream 中,并重复这个过程 count 次。
fread() 函数
用于从文件中按字节读取数据块,返回实际成功读取的数据块个数。
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
ptr:指向存储读取数据的内存块的指针。
size:每个数据块的字节数。
count:要读取的数据块个数。
stream:指向要读取的文件的指针。
使用 fread() 函数时,它会从指定的文件流 stream 中读取 size 字节的数据块,并重复这个过程 count 次,将读取的数据存储到 ptr 指向的内存块中。
fclose()函数
用于关闭打开的文件流,若成功关闭文件,则返回0;若关闭文件失败,则返回非零值。
int fclose(FILE *stream);
使用 fclose() 函数时,它会关闭指定的文件流,并刷新缓冲区中的数据。在关闭文件之前,它会将缓冲区中的数据写入到文件中。
fflush()函数
用于刷新输出缓冲区。若成功刷新缓冲区,则返回0;若刷新缓冲区失败,则返回非零值。
int fflush(FILE *stream);
参数说明:
stream:指向要刷新缓冲区的文件的指针。如果传入 NULL,则会刷新所有打开的文件流的缓冲区。
使用 fflush() 函数时,它会将输出缓冲区的内容立即写入到文件中(对于输出流)或者屏幕上(对于标准输出流 stdout)。这个函数通常用于确保数据被及时写入,而不是等到缓冲区满或者程序结束时才自动刷新。
fsync()函数
用于将文件数据同步到磁盘上的永久存储空间。若成功同步到磁盘,则返回0;若同步失败,则返回非零值。
int fsync(int fd);
参数说明:
fd:要同步到磁盘的文件描述符。
使用 fsync() 函数时,它会强制将文件缓冲区中的数据写入到磁盘上的永久存储空间。这个函数主要用于确保在系统崩溃或断电等异常情况下,文件数据不会丢失或损坏。
需要注意的是,fsync() 函数的调用可能会导致性能下降,因为它会强制进行磁盘写入操作。
setbuf()函数
用于在打开的文件上设置自定义的缓冲区。
void setbuf(FILE *stream, char *buffer);
参数说明:
stream:指向要设置缓冲区的文件的指针。
buffer:指向用于设置缓冲区的字符数组的指针。如果传入 NULL,则禁用缓冲区。
使用 setbuf() 函数时,它可以用于将自定义的字符数组作为缓冲区与文件关联。缓冲区提供了一种临时存储数据的方式,可以提高文件的读写性能。
需要注意的是,如果传入的 buffer 参数为 NULL,则会禁用缓冲区,即设置为无缓冲模式。在无缓冲模式下,每次写入或读取都会立即进行 I/O 操作,而不会暂存数据。这种模式适合于需要实时数据交换或者文件较小的情况。
write()函数
用于将数据写入文件或文件描述符的系统调用函数。成功时,返回写入的字节数。失败时,返回-1。
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd:文件描述符,表示要写入的目标文件或设备。
buf:指向要写入数据的缓冲区的指针。
count:要写入的字节数。
write() 函数会尽可能将指定数量的字节从缓冲区 buf 写入到文件或设备中。它是一个阻塞函数,即在数据完全写入之前会一直等待。
fwrite 与 write 的区别
- fwrite() 是库函数,默认使用缓冲区,即将数据先写入缓冲区,可以使用 setbuf() 或 setvbuf()
函数自定义缓冲区。缓冲区可以提高写入效率,在遇到文件关闭、缓冲区满、调用 fflush() 等情况时会触发真正的写入操作,一次写盘
- write() 函数 是系统调用,无缓冲的,数据直接写入到磁盘,涉及用户态和内核态的切换。
fwrite通过fflush(内部触发 write )把用户缓冲区数据刷新到内核缓冲区中,通过fsync把内核缓冲区数据刷新到磁盘。
参考
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务