Unix域协议
socket套接字也可以实现一台主机上的不同进程间通信。
socket--->IPC手段
socket实现进程间通信用到了另外一个协议:
Unix域协议,简称AF_UNIX或AF_LOCAL
Unix与协议提供了两类套接字:
SOCK_STREAM 字节流套接字(类似TCP)
SOCK_DGRAM 数据报套接字(类似UDP)
虽然UDP协议是不可靠的,但是Unix域数据报服务是可靠的。
不会丢失消息,也不会传递出错,因为本质上就是在同一台主机上面进行通信。
Unix域协议的区别:
1. 和TCP/UDP相比,速度更快,数据不需要传递到主机外,也不需要封包和拆包的过程。
2. 相对于IP协议来讲,IP协议是通过IP地址+端口号标识客户端和服务器
Unix域协议是使用普通文件系统中的路径名标识客户端和服务器。
Unix域协议大部分的编程流程和函数API结构都与TCP类似,
只不过Unix域协议使用路径名描述一个地址
//
#include <sys/un.h>
define UNIX_PATH_MAX 108
//Unix域协议的地址结构体
struct sockaddr_un
{
sa_familt_t sun_family;//AF_UNIX/AF_LOCAL
char sun_path[UNIX_PATH_MAX];//Unix域协议地址,是以'\0'为结尾的本地文件系统
//以绝对路径形式存在。“/home/china/long”
};
Unix域协议字节流套接字编程方法(类似于TCP编程):
Server:先创建套接字---> bind ---> 监听 ---> 接收客户端的连接请求 ---> 连接成功
---> read/write ---> close
Client: 先创建套接字 ---> connect ---> read/write ---> close
做一个DNS客户端向DNS服务器提交请求解析某一个某一个域名对应的IP地址。
五、IO模型
IO模型---->输入输出模型
可以把IO(输入输出)理解为两个步骤:
1. 等待IO事件就绪。
如果可以读,我们等待缓冲区内有数据就可以读,如果可以写,我们就等到缓冲区内有空间
可以写。
2. 第二步才是真正意义上的数据的迁移。
涉及到内核态和用户态的切换。
Linux中五种IO模型:
1) 阻塞IO
读:如果有数据(即使数据少于你要读取的字节数),直接返回数据
如果没有数据,则阻塞(等待IO事件就绪),直到有数据或者出错
写:如果有空间(即使空间少于你要写的字节数)可以写,直接写入
如果没有空间让你写,则阻塞直到有空间写或者出错。
这种模型是最常见的,最简单的,效率最低的一种IO模型,是默认的IO模型
read/write/sendto/recvform....都是属于阻塞IO。
2) 非阻塞IO
能读(有数据)就读,不能读(没有数据)就立即返回一个错误码。
能写(有空间)就写,不能写(没有空间)就立即返回一个错误码。
虽然这种不会阻塞,但是有一个缺点,这种方式相对于阻塞IO可能会更加效率低。
会浪费掉大量的CPU。
非阻塞IO采用的方式有点类似于轮询,所以效率可能会更低。
3) IO多路复用
允许同时对多个IO事件进行控制,同时监控多个“文件描述符”。
IO多路复用实现的机制是通过select/poll/epoll函数来实现的。
4) 信号驱动IO
如果有IO事件就绪了,就可以发一个信号给应用程序,进行数据的处理。
相当于注册了一个信号处理函数,当数据准备好了,就给我发一个信号
当我捕捉到这个信号之后,就去我的信号处理函数中去获取数据即可。
所以信号驱动IO是一种非阻塞IO。
5) 异步IO
应用的较少。
进程仅仅是发起对数据的请求,而数据的监控和迁移由别的进程来完成。
从阻塞程度上来说:
阻塞IO > 非阻塞IO > 多路复用IO > 信号驱动IO > 异步IO
1. 文件的读写方式(阻塞/非阻塞)的改变
文件的默认读写方式是阻塞。
在open的时候加上O_NOBLOCK选项,则可以以非阻塞的形式打开文件。
通过fcntl函数去改变一个已经打开的文件的文件性质的。
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl改变文件的状态,具体的操作由命令号cmd来决定。
fcntl有五种功能:
1. 复制一个现有的文件描述符(cmd == F_DUPFD)
让多个文件描述符指向同一个文件。
复制一个现有的文件描述符fd,新的文件描述符作为函数的返回值返回。
比如:
int new_fd = fcntl(old_fd,F_DUPFD,4);
第三个参数表示新的文件描述符的最小值。
如果如果4被占用了,那么新的文件描述符就+1,直到找到一个没有被占用。
2. 设置/获取文件描述符标志(cmd == F_GETFD/F_SETFD)
cmd == F_GETFD,第三个参数不需要设置
作用是取得与文件描述符关联的状态标志,目前与文件描述符关联的
状态标志只有一个close_on_exec(FD_CLOEXEC).
FD_CLOEXEC的作用是:
是一个进程所有的文件描述符的位图标志,每一个比特代表一个打开的
文件描述符,用于确定在调用exec时需要关闭的文件描述符。
如果对应的标志位置为1,则子进程在执行exec时该描述符将会被关闭
如果对应的标志位置为0,则子进程在执行exec时该描述符将会维持被打开的状态
cmd == F_SETFD,第三个参数为新的文件描述符的状态标记
3. 获取/设置文件状态标志(cmd == F_GETFL/F_SETFL)
F_GETFL :
第三个参数不需要设置
文件所有的状态标记作为返回值返回
文件的状态标记有:
O_RDONLY/O_WRONLY/O_RDWR/O_APPEND/O_NOBLOCK.....
这些标记,保存在一个struct file结构体的一个成员变量中:
unsigned long f_flags;
是通过位域来实现,各个状态只是变量中的某一位。
F_SETFL:新的文件状态标志按照第三个参数设置
例子:
判断一个打开的文件,是否为非阻塞
unsigned long f_flags = fcntl(fd,F_GETFL);
if(f_flags & O_NOBLOCK)
{
//文件是阻塞的
}
else
{
//文件是非阻塞的
}
设置文件的阻塞方式
unsigned long f_flags = fcntl(fd,F_GETFL);
f_flags &= ~O_NOBLOCK;//去掉非阻塞标志,其他标志不动
fcntl(fd,F_SETFL,f_flags);
4. 获取/设置文件的属主进程
5. 获取/设置文件记录锁
当一个进程正在修改或者读取文件的时候,可以阻止另一个进程去访问文件。
3) 多路复用IO
用来实现对多个文件描述符进行IO“监听”。
一般用在网络服务器中,可以并发的处理多个客户端的连接和请求。
1. select
实现原理:
1. 将需要监听的文件描述符放在一个监听集合中,将这个集合拷贝到内核中去。
2. 在内核中创建一个内核线程,有这个线程去轮询所有的文件描述符,这个线程
是处于内核空间,所有这个CPU占用的是内核的执行时间,而不是占用用户的
执行时间,当一个或者多个文件描述符就绪(可读了,可写了,出错了)时,
就将就绪的文件描述符集合拷贝到用户空间中去。
3. 用户去处理就绪的文件描述符。
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
将fd指定的文件描述符从set指定的集合中移除。
int FD_ISSET(int fd, fd_set *set);
判断fd指定的文件描述符是否存在于set指定的集合中
void FD_SET(int fd, fd_set *set);
把fd指定的文件描述符加入到set指定的集合中
void FD_ZERO(fd_set *set);
把set指向的文件描述符集合清空
select用类型fd_set来表示一个文件描述符的集合,因为你需要监听多个文件描述符
需要一个fd_set类型的集合来保存你所有需要监听的文件描述符。
但是可能有的文件描述符是监听可读,有的文件描述符是监听可写,
有的监听是否出错,所有,就有三个文件描述符集合分别存储你要监听的
文件描述符。
readfds:监听是否可读的文件描述符集合
writefds:监听是否可写的文件描述符集合
exceptfds:监听是否出错的文件描述符集合
参数:
nfds:指这个集合中所有文件描述符的范围
或者说监听的文件描述符的个数。
你需要监听的所有文件描述符中的最大值+1
内核是从0文件描述符开始轮询,到你指定的最大的文件描述符。
readfds:监听可读的文件描述符集合
返回的时候,里面保存的是所有的已经可读的文件描述符
writefds:监听可写的文件描述符集合
返回的时候,里面保存的是所有的已经可写的文件描述符
exceptfds:监听出错的文件描述符集合
返回的时候,里面保存的是所有的已经出错的文件描述符
上面三个集合,如果不需要监听则可指定全部为NULL
select(max_fd,NULL,NULL,NULL,2);//sleep(2)
select(nfds,&readfds,NULL,NULL,&timeout);
timeout:超时时间,在指定的时间内还没有文件描述符就绪,也返回
struct timeval
{
long tv_sec; //秒
long tv_usec; //毫秒
};
等待分为三种情况:
1. 如果把这个参数设置为NULL,表示一直等待下去,“死等”
直到至少有一个文件描述符继续。
2. 等待一段固定的时间(有timeval参数指定等待多久)。
在等待的时间内,正常等待,一旦超时,还没有文件描述符就绪的
话,就立即返回。同时超时时间全部清0。
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
select(nfds,&readfds,NULL,NULL,&timeout);//只监听是否可读集合2秒
//tv_sec == 0,tv_usec == 0
3. 根本不等待。
轮询一次后立即返回。
该参数必须指定struct timeval中的成员变量的值都为0。
返回值:
> 0 表示已经就绪的文件描述符的个数
由于你监听了多个文件描述符,至于你到底是哪些文件描述符就绪了
你必须在select返回后,使用FD_ISSET一个一个的去测试。
== 0 超时了,等待的指定的时间到了。
< 0 表示select函数出错了,errno被设置。
简单的伪代码逻辑:
fd_set readfds;//需要监听可读的文件描述符的集合
int max_fd = 0;
struct timeval timeout;
while(1)
{
//把所有需要监听的文件描述符加入到集合
//select每次返回的时候,集合里面只保存了就绪的文件描述符所以你每次
//重新监听前需要重新将你想要监听的文件描述符加入到集合中去
//将文件描述符集合清空
FD_ZERO(&readfds);
while(...)//不断的把你想要监听的文件描述符加入到集合中去
{
FD_SET(fd,&readfds);
max_fd = (fd >= max_fd) ? fd : max_fd;//记录所有加入集合的文件描述符的最大值
}
timeout.tv_sec = 10;//等待10秒
timeout.tv_usec = 0;
//使用select监听指定的集合
int r = select(max_fd + 1,&readfds,NULL,NULL,&timeout);
if(r == 0)//超时了
{
continue;
}
else if(r < 0)
{
perror("select error");
return -1;//continue;
}
//r > 0 有就绪的文件描述符啦
while(...)//所有加入集合的文件描述符都需要遍历
{
if(FD_ISSET(fd,&readfds))
{
//fd是可读
//对fd进行读
}
}
}
2. poll
poll的原理基本上和select类似,poll是在内核中使用链表来存储文件描述符集合
而select是使用数组去存储(数组空间有限,所以最多监听1024个文件描述符)。
poll监听的文件描述符个数不限。
NAME
poll, ppoll - wait for some event on a file descriptor
在文件描述符上等待一些事件
SYNOPSIS
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll与select类似,但是poll是用结构体struct pollfd描述
你要监听的文件描述符以及你要监听的事件。
struct pollfd {
int fd; /* file descriptor */
//你要监听的文件描述符
short events; /* requested events */
//你要监听的文件描述符的事件
//你所期待文件描述符发生的事件,事件码使用位域来实现。
//POLLIN:期待文件描述符可读
//POLLOUT:期待文件描述符可写
//POLLERR:期待文件描述符出错
如:
POLLIN | POLLOUT 监听是否可读可写
short revents; /* returned events */
//已经就绪的事件
如:是否已经可读
if(revents & POLLIN)
{
//可读啦
}
};
监听一个文件描述符的需要一个这样的结构体。
如果需要监听多个文件描述符的话就需要多个这样的结构体。
fds:struct pollfd *的指针,指向你要监听的所有的事件。
实际上就是监听结构体struct pollfd的数组
nfds:表示上面那个数组中的元素个数
timeout:超时时间,单位是ms
返回值:
> 0 表示就绪的文件的个数
由于你监听多个文件描述符,至于到底是哪些文件描述符就绪了、
你必须在poll返回后,一个一个的去测试每一个结构体中的
revents成员变量。
= 0 超时了
< 0 出错了
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
与poll类似,时间更加精确(微妙级),去屏蔽一些信号。
简单的伪代码:
while(1)
{
struct pollfd fds[100];//需要监听的事件集合数组
for(i = 0;i < 你需要监听的文件描述符个数;i++)
{
fds[i].fd = fd;
fds[i].event = POLLIN | POLLOUT;//监听fd是否可读可写
fds[i].revents = 0;//初始化的时候成员变量没有值
}
//多路复用poll
int r = poll(fds,100,2000);
if(r < 0)
{
perror("poll failed");
return -1;//continue;
}
else if(r == 0)
{
printf("timeout!\n");
continue;
}
//表示有文件描述符就绪了,就绪的文件描述符就保存在fds中
//但是你不知道是哪一个就绪了,所以需要测试
for(i = 0;i < 100;i++)
{
if(fds[i].revents & POLLIN)
{
//fds[i].fd这个文件描述符可读了
//read
}
else if(fds[i].revents & POLLOUT)
{
//fds[i].fd这个文件描述符可写了
//write
}
}
}
3. epoll
select和poll的效率都不高。
1. 因为这些文件描述符每一次执行函数(select/poll)都会被拷贝两次。
2. select和poll每次需要去遍历所有的文件描述符才能确定哪些文件描述符就绪了。
epoll不会随着文件描述符的数量增加而降低效率。在返回的时候,只返回一个就绪队列。
epoll的实现接口函数,只有三个函数:
1) 创建epoll的句柄
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
创建/打开一个监听文件的集合,这个函数的返回值是一个文件描述符。
size:用来告诉内核这个监听的数量一共有多大。
现在这个参数其实是被忽略的,意思就是你只要填>0的数
就意味着监听任意多个文件描述符。
同时epoll本身也会占用一个文件描述符,使用完毕后需要关闭掉它。
返回值:成功返回一个epoll的实例(本质就是一个文件描述符),失败返回NULL
同时errno被设置。
2) 将需要监听的文件描述符加入到epoll句柄或者从句柄中删除要监听的文件描述符
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
对于指定的需要监听的文件描述符,只需要加入一次即可。
会记录到内核链表中去,不想select和poll每一次监听都需要重新加入。
epfd:epoll的实例,表示你要操作哪一个epoll的文件描述符
op:具体对epoll实例进行何种操作
EPOLL_CTL_ADD:增加fd指定的文件描述符到epfd所表示的实例中去
EPOLL_CTL_MOD:修改fd指定的文件描述符监听的事件
EPOLL_CTL_DEL:从监听集合中删除fd指定的文件描述符
fd:要操作(ADD/MOD/DEL)的文件描述符
event:要监听fd的哪一些事件
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
//事件描述结构体
struct epoll_event {
uint32_t events; /* Epoll events */
//要监听的事件,使用位域来实现,不同的事件占events不同的bit位
//主要要监听的事件由:
//EPOLLIN : 监听的事件为可读事件
//EPOLLOUT : 监听的事件为可写事件
//EPOLLERR : 监听的事件是否出错
//EPOLLRDHUP : 监听流式套接字(UDP)对方是否关闭
//EPOLLLET : Edge-Triggered
//边缘触发
//这个标志表示监听的文件的数据有变化时,才会报告事件
//有两种模式:
//LT:Level-Triggered 只要有数据,就会不停的上报可读事件
//默认行为为LT。
//ET:Edge-Triggered 只有当有数据变化(数量)的时候,
//才会报告可读事件
//LT:不停的上报可读事件
//ET:只有在数据的数量发生变化的时候才会上报事件
epoll_data_t data; /* User data variable */
//用户自定义的数据,用来保存用户的一些数据
};
成功返回0,失败返回-1,同时errno被设置。
3) 等待监听事件的发生
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epfd:需要监听的epoll的实例。
events:指针,指向一个结构体数组,用来保存已经就绪的事件的信息。
maxevents:表示第二个参数结构体数组的最多可以保存多少个事件结构体。
timeout:超时时间,单位ms
返回值:
>0 已经就绪的文件描述符的个数
已经就绪的事件的信息是直接保存在events指向的数组中
struct epoll_event结构体中有一个成员变量,可以保存用户的数据
一般这个成员变量用来保存要监听的文件描述符本身。
当函数返回时,直接去操作事件结构体就可以了,不需要轮询所有的文件
描述符。
=0 超时了
<0 出错,同时errno被设置。
简单的伪代码:
int epollfd = epoll_create(10);
struct epoll_event ev;
//只需要添加一次即可
while(....)//可能有很多文件描述符需要添加
{
ev.events = EPOLLIN;//记录监听的事件
ev.data = fd;//记录事件本身的文件描述符
epoll_ctl(epollfd,EPOLL_CTL_ADD,&event);
}
while(1)
{
struct epoll_event e[10];//记录监听结果的结构体数组
int r = epoll_wait(epollfd,e,10,2000);
if(r == 0)
{
printf("time out!\n");
continue;
}
else if(r < 0 )
{
perror("epoll failed");
return -1;
}
//所有的就绪信息保存到了e数组中
//不需要轮询所有的文件描述符
for(i = 0;i < r;i++)
{
//e[i].events 就是表示就绪的事件
//e[i].data.fd 就是表示是哪一个文件描述符就绪了
if(e[i].events & EPOLLIN)
{
//表示e[i].data.fd文件描述符可读
}
else if(e[i].events & EPOLLOUT)
{
//表示e[i].data.fd文件描述符可写
}
}
}
项目:
实现一个简单的FTP服务器文件传输助手
功能:通过网络传输,实现文件的跨设备传输,包含服务器和客户端
服务器功能:
等待客户端的连接,支持多客户端并发,根据客户端发送过来的命令,执行
相应的操作,并向客户端发送其所需要的数据。
客户端功能:
负责连接服务器后,发服务器发送命令(命令从键盘获取),并等待服务器的
相应,同时处理服务器回复的数据。
命令:
1. ls
用来获取服务器目录下的文件信息(文件名)
2. get file
用来从服务器中获取指定的文件名的文件
如果服务器存在此文件,则服务器回复文件内容
如果服务器不存在该文件,则回复错误码
3. put file
用来上传文件到服务器
如果服务器愿意接受文件,则后续发送文件数据
如果服务器不愿意接受数据,则不发送
4. bye
用来告诉服务器,客户端将要断开连接
发送bye之后,客户端关闭套接字
服务器收到bye之后,关闭与客户端连接的套接字,并结束该客户端处理分支
详情见ftp服务器