ftp实现
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
功能要求:
1.项目基于tcp连接进行编写
-
客户端命令行传参,传入ip、port、文件路径,实现把指定目录下的文件发送到服务器
-
服务器接收并放到指定文件路径

linux下IO模及其特点
场景假设

假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
-
进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了
-
时不时进房间看一下:简单,空闲时间还能干点别的,但是很累
-
妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误
一、Linux下四种模型的特点:
阻塞式IO 非阻塞式IO 信号驱动IO(了解) IO多路复用(帮助TCP实现并发)
1、阻塞式IO(BIO)
特点:简单、常用、效率低
● 当程序调用某些接口时,如果期望的动作无法触发,那么进程会进入阻塞态(等待状态,让出CPU的调度),当期望动作可以被触发了,那么会被唤醒,然后处理事务。
● 重点理解相对于进程而言的影响;
● 阻塞I/O模式是最普遍使用的I/O模式,大部分程序使用的都是阻塞模式的I/O 。
● 前面学习的很多读写函数在调用过程中会发生阻塞。
`阻塞I`/`O 模式是最普遍使用的I`/`O 模式,大部分程序使用的都是阻塞模式的I`/`O 。
缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I`/`O 模式。
学习的读写函数在调用过程中会发生阻塞相关函数如下:
•读操作中的read、recv、recvfrom
读阻塞`--`》需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write、send
写阻塞`--`》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
注意:sendto没有写阻塞
`1`)无sendto函数的原因:
sendto不是阻塞函数,本身udp通信不是面向连接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
`2`)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
•其他操作:accept、connect
`
udp与tcp缓存区 仅作为了解
UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。
丢包出现原因: 接收缓存区满 网络拥堵, 传输错误
相比之下,TCP是一种面向连接的传输协议,它需要保证数据的可靠性和顺序性。TCP有发送缓存区和接收缓存区, 如果发送频率过快, 且内容小于发送缓存区的大小 , 可能会导致多个数据的粘包。如果发送的数据大于发送缓存区, 可能会导致拆包。
UDP不会造成粘包和拆包, TCP不会造成丢包
UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将UDP的每个数据包分隔开, 不会造成粘包。
TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;
TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)
TCP:

UDP:

2、非阻塞式IO(NIO)
特点:可以处理多路IO;需要轮询,浪费CPU资源

`•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:"当我请求的I`/`O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。"
•(引导着让大家说出来)当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。`---`轮询
•应用程序不停的polling 内核来检查是否I`/`O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
`
如何设置非阻塞
-
通过函数自带参数设置
-
通过设置文件描述符属性fcntl (file control)
`#include <unistd.h>
#include <fcntl.h>
`int` `fcntl(int` fd`,` `int` cmd`,` `...);`
功能:
获取`/`改变文件属性`(`linux中一切皆文件`)`
文件描述符:stdin 0、stdout 1`、`stderr 2
参数:fd:文件描述符
cmd: 操作功能选项 (可以定义个变量,通过vi -t F_GETFL 来找寻功能赋值 )
F_GETFL:获取文件描述符的原有的状态信息
//不需要第三个参数,返回值为获取到的属性
F_SETFL:设置文件描述符的状态信息 - 需要填充第三个参数
//需要填充第三个参数 O_RDONLY, O_RDWR ,O_WRONLY ,O_CREAT
O_NONBLOCK 非阻塞 O_APPEND追加
O_ASYNC 异步 O_SYNC 同步
F_SETOWN: 可以用于实现异步通知机制。
//当文件描述符上发生特定事件时(例如输入数据到达),内核会向拥有该 文件描述符的进程发送 SIGIO 信号(异步),以便进程能够及时处理这些事件。
第三个参数:由第二个参数决定,set时候需要设置的值,get时候填0
arg:文件描述符的属性 ----------同上参数,一般填0
返回值: 特殊选择:根据功能选择返回 (int 类型)
其他: 成功0 失败: -1;
设置流程:
int flag;//文件状态的标志
flag = fcntl(fd, F_GETFL); //读
flag |= O_NONBLOCK;//改 O_NONBLOCK = 0x00004000
fcntl(fd, F_SETFL, flag);//写
`

3、信号驱动IO(异步IO模型 非重点)
特点:异步通知模式,需要底层驱动的支持
`操作系统中的同步与异步
在操作系统中,特别是在Linux中,同步和异步是描述I`/`O操作方式的两个概念。它们主要区分在于操作完成的通知方式和程序执行的流程。
同步(Synchronous):
同步I`/`O操作是指在执行I`/`O操作时,程序必须等待操作完成才能继续执行。在同步操作中,程序提交一个I`/`O请求后,操作系统会阻塞该程序,直到请求操作完成。此时,程序才能继续执行后续的代码。因此,同步操作会导致程序执行流程暂停,直至I`/`O操作完成。
同步I`/`O的例子:`read(),` `write(),` `recv(),` `send()` 等。
异步(Asynchronous):
异步I`/`O操作是指程序在发起I`/`O请求后,无需等待操作完成,可以继续执行其他任务。当异步I`/`O操作完成时,程序会通过某种方式(如回调函数、事件通知、信号等)得到通知。因此,异步操作使程序执行流程得以继续,而不必等待I`/`O操作完成。
`
● 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
SIGIO`
文件描述符准备就绪`,` 可以开始进行输入`/`输出操作`.`
`
● 应用程序收到信号后做异步处理即可。
● 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
● 标准模板
//将APP进程号告诉驱动程序`
`fcntl(`fd`,` F_SETOWN`,` `getpid());`
`//使能异步通知`
`int` flag`;`
flag `=` `fcntl(`fd`,` F_GETFL`);`
flag `|=` O_ASYNC`;` `//也可以用FASYNC标志`
`fcntl(`fd`,` F_SETFL`,` flag`);`
`signal(`SIGIO`,` handler`);`
`
signal信号处理相关函数
头文件: #include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler)
功能:信号处理函数(注册信号)
参数: int signum:要处理的信号(要修改的信号)
sighandler_t handler: 函数指针: void(*handler)(int) (修改的功能:)
handler:------void handler(int num) 自定义的信号处理函数指针
返回值: 成功:设置之前的信号处理方式
失败: SIG_ERR
用非阻塞方式监听鼠标的数据
查看自己使用的鼠标:/dev/input
检查鼠标设备:sudo cat /dev/input/mouse0
注意:执行的时候需要加sudo
`#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
`int` fd`;`
#define N 64
`char` buf`[`N`]` `=` `{0};`
`void` `handler(int` sig`)`
`{`
`int` ret`;`
ret `=` `read(`fd`,` buf`,` N`);`
`if` `(`ret `<` `0)`
`{`
`perror(`"READ ERR."`);`
`return;`
`}`
`else`
`{`
`printf(`"len= %d\n"`,` ret`);`
`}`
`}`
`int` `main(int` argc`,` `char` `const` `*`argv`[])`
`{`
fd `=` `open(`"/dev/input/mouse0"`,` O_RDONLY`);`
`if` `(`fd `<` `0)`
`{`
`perror(`"open err"`);`
`return` `-1;`
`}`
`// 将APP进程号告诉驱动程序`
`fcntl(`fd`,` F_SETOWN`,` `getpid());`
`// 使能异步通知`
`int` flag`;`
flag `=` `fcntl(`fd`,` F_GETFL`);`
flag `|=` O_ASYNC`;` `// 也可以用FASYNC标志`
`fcntl(`fd`,` F_SETFL`,` flag`);`
`signal(`SIGIO`,` handler`);`
`while` `(1)`
`{`
`printf(`"-----------\n"`);`
`sleep(1);`
`}`
`close(`fd`);`
`return` `0;`
`}`
`
4.IO多路复用
4.1、IO多路复用场景假设
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
- 挨个房间跑
4.2、IO多路复用机制
I/O多路复用 - 帮助TCP实现并发服务器
-
进程中若需要同时处理多路输入输出 ,在使用单进程和单线程的情况下, 可使用IO多路复用处理多个请求;
-
IO多路复用不需要创建新的进程和线程, 有效减少了系统的资源开销。
场景:就比如服务员给50个顾客点餐,分两步:
顾客思考要吃什么(等待客户端数据发送)
顾客想好了,开始点餐(接收客户端数据)

要提高效率有几种方法?
-
安排50个服务员 (类似于多进程/多线程实现服务器连接多个客户端,太占用资源)
-
哪个顾客想好了吃啥, 那个顾客来柜台点菜 (类似IO多路复用机制实现并发服务器)
实现IO多路复用的方式: select poll epoll
基本流程是:
-
先构造一张有关文件描述符的表;
-
清空表
-
将你关心的文件描述符加入到这个表中;
-
调用select函数。
-
判断是哪一个或哪些文件描述符产生了事件(IO操作);
-
做对应的逻辑处理;
● 使用I/O多路复用技术。其基本思想是:
○ 先构造一张有关描述符的表,然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
○ 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。
`基本流程:
`1.` 先构造一张有关文件描述符的表`(`集合、数组`);`
`2.` 将你关心的文件描述符加入到这个表中`;`
`3.` 然后循环调用一个函数。 select `/` poll
`4.` 当这些文件描述符中的一个或多个已准备好进行I`/`O操作的时候
该函数才返回`(`阻塞`)`。
`5.` 判断是哪一个或哪些文件描述符产生了事件`(`IO操作`);`
`6.` 做对应的逻辑处理`;`
`
4.3、select :用于监测是哪个或哪些文件描述符产生事件;
`#include<sys/select.h>
#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`);`
功能:select用于监测是哪个或哪些文件描述符产生事件`;`
参数:
nfds:监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)
(这里是个数,使用的时候注意,与文件中最后一次打开的文件描述符所对应的值的关系是什么?)
readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合; //NULL 表示不关心
timeout: 设为NULL,等待直到某个文件描述符发生变化;
设为大于0的值,有描述符变化或超时时间到才返回。
超时时间检测:如果规定时间内未完成函数功能,返回一个超时的信息,我们可以根据该信息设定相应需求;
如果设置了超时检测时间:`&`tv
select返回值:
`<0` 出错
`>0` 表示有事件产生`;`
`==0` 表示超时时间已到`;`
`struct` `timeval`
`{`
`long` tv_sec`;` `/* seconds */`以秒为单位,指定等待时间
`long` tv_usec`;` `/* microseconds */`以毫秒为单位,指定等待时间
` };`
`void` `FD_CLR(int` fd`,` fd_set `*`set`);//将fd从表中清除`
`int` `FD_ISSET(int` fd`,` fd_set `*`set`);//判断fd是否在表中`
`void` `FD_SET(int` fd`,` fd_set `*`set`);//将fd添加到表中`
`void` `FD_ZERO(`fd_set `*`set`);//清空表1`
`
select特点:
1. 一个进程最多 只能监听1024个文件描述符 (32位) [64位为 2048]
2. select被唤醒之后要重新轮询 (0-1023)一遍驱动,效率低(消耗CPU资源)
3. select每次会清空未响应的文件描述符,每次都需要拷贝用户空间的表到内核空间,效率低,开销较大
(0~3G是用户态,3G~4G是内核态,两个状态来回切换 拷贝是非常耗时,耗资源的)
select机制(辅助理解):
-
头文件检测1024个文件描述符 0-1023
-
在select中0~2存储标准输入、标准输出、标准出错
-
监测的最大文件描述个数为fd+1(如果fd = 3,则最大为 4) : //因为从0开始的
-
select只对置1的文件描述符感兴趣 假如事件产生,select检测时 , 产生的文件描述符会保持1,未产生事件的会置0;
-
select每次轮询都会清空表(置零的清空) //需要在select前备份临时表



练习1: 如何通过select实现 响应鼠标事件同时响应键盘事件
`#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
`//响应鼠标的时候, 打印鼠标事件 `
`//输入键盘的时候, 打印键盘内容`
`int` `main(int` argc`,` `char` `const` `*`argv`[])`
`{`
`//1.打开鼠标文件`
`int` fd `=` `open(`"/dev/input/mouse0"`,`O_RDONLY`);`
`if(`fd `<` `0)`
`{`
`perror(`"open is err:"`);`
`return` `-1;`
`}`
`//1.创建文件描述符的表`
fd_set readfds`,`tempfds`;`
`//2.清空表`
`FD_ZERO(&`readfds`);`
`//3.添加关心的文件描述符`
`FD_SET(0,&`readfds`);`
`FD_SET(`fd`,&`readfds`);`
`int` maxfd `=` fd`;`
`char` buf`[128];`
`while(1)`
`{`
tempfds `=` readfds`;`
`//4.select检测 阻塞`
`select(`maxfd`+1,&`tempfds`,`NULL`,`NULL`,`NULL`);`
`if(FD_ISSET(0,&`tempfds`))`
`{`
`//1.键盘`
`fgets(`buf`,sizeof(`buf`),`stdin`);`
`if(`buf`[strlen(`buf`)-1]` `==` '\n'`)`
buf`[strlen(`buf`)-1]` `=` '\0'`;`
`printf(`"key: %s\n"`,`buf`);`
`}`
`if(FD_ISSET(`fd`,&`tempfds`))`
`{`
`//2.鼠标`
`int` ret `=` `read(`fd`,`buf`,sizeof(`buf`));`
buf`[`ret`]` `=` '\0'`;`
`printf(`"mouse: %s\n"`,`buf`);`
`}`
`}`
`close(`fd`);`
`return` `0;`
`}`
`
练习:select实现客户端服务器全双工通信并发服务器的建立
`在tcp的服务器端`,` 有两类文件描述符
监听的文件描述符
`1.`只需要有一个
`2.`不负责和客户端通信`,` 负责检测客户端的连接请求`,` 检测到之后调用accept就可以建立新的连接
通信的文件描述符
`1.`负责和建立连接的客户端通信
`2.`如果有`N`个客户端和服务器建立了新的连接`,` 通信的文件描述符就有`N`个,每个客户端和服务器都对应一个通信的文件描述符
`
总结select实现IO多路复用特点
`1. 一个进程最多只能监听1024个文件描述符 (千级别)
2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);
4.跨平台
`
(1)客户端
`/*客户端创建代码 */
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// #include "head.h"
enum type_t
{
login, //登录
chat, //发送信息
quit, //退出
};
typedef struct mag_t
{
int type; //功能
char name[32]; //ip
char text[128]; //内容
} MSG_t;
int main(int argc, char const *argv[])
{
if (argc < 3)
{
printf("plase input <ip><port>");
return -1;
}
//1.创建套接字,用于链接
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
//2.填充结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//协议族
saddr.sin_port = htons(atoi(argv[2]));//端口
saddr.sin_addr.s_addr = inet_addr(argv[1]);//IP
MSG_t msg; //消息包
socklen_t len = sizeof(saddr); //结构体大小
int num=0;//交互次数
pid_t pid = fork();//创建父子进程
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0) //子进程接收消息
{
while (1)
{
//接受信息
if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, &len) < 0)
{
perror("recvfrom err");
return -1;
}
printf("ip:%s 状态:%d 内容:%s\n", msg.name, msg.type, msg.text);
}
}
else //父进程发送消息
{
while (1)
{
strncpy(msg.name, "xiaoyang", 8);//客户端昵称
//发送信息
memset(msg.text, 0, sizeof(msg.text)); //清空数组内容
printf("发送内容:");
fgets(msg.text, sizeof(msg.text), stdin); //从终端获取内容存放到数组中
if (strncmp(msg.text, "quit", 4) == 0) //输入quit退出客户端
{
msg.type = quit;//退出状态
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
exit(0);
}
if (msg.text[strlen(msg.text)] == '\0')
{
msg.text[strlen(msg.text) - 1] = '\0';
}
if (num == 0) //第一次登入
{
msg.type == login;//登录状态
}
else
{
msg.type = chat;//交互状态
}
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);//发送信号
}
}
close(sockfd);
return 0;
}
`
(2) 服务器
`/*服务器创建代码 */
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
enum type_t
{
login,//登录
chat,//发送信息
quit,//退出
};
typedef struct mag_t
{
int type;//功能
char name[32];//ip
char text[128];//内容
} MSG_t;
MSG_t msg;
//链表节点结构体
typedef struct node_t
{
struct sockaddr_in addr;//ip地址
struct node_t *next;//链表下一个地址
}list_t;
int main(int argc, char const *argv[])
{
if (argc < 2)
{
printf("plase input <ip><port>\n");
return -1;
}
//1.创建套接字,用于链接
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
//2.绑定 ip+port 填充结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(saddr); //结构体大小
//bind绑定ip和端口
if (bind(sockfd, (struct sockaddr *)&saddr, len) < 0)
{
perror("bind err");
return -1;
}
printf("bind success\n");
// char buf[128] = {0};
while (1)
{
//接收信息
if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, &len) < 0)
{
perror("recvfrom err");
return -1;
}
switch (msg.type)
{
case login:
Loginrecv();break;
case chat:
Chatrecv();break;
case quit:
Quitrecv();break;
}
//发送信息
printf("server:");
fgets(msg.text, sizeof(msg.text), stdin); //从终端获取内容存放到数组中
if (strncmp(msg.text, "quit", 4) == 0) //输入quit退出客户端
{
break;
}
if (msg.text[strlen(msg.text)] == '\0')
{
msg.text[strlen(msg.text) - 1] = '\0';
}
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
}
close(sockfd);
return 0;
}
void Chatrecv()//chat 型
{
// printf("client ip:%s ,port:%d buf:%s\n", inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port),msg.text);
printf("ip:%s 状态:chat 内容:%s\n", msg.name,msg.text);
}
void Loginrecv()//login 型 首次链接
{
}
void Quitrecv()//quit 退出
{
//接收信息
if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, &len) < 0)
{
perror("recvfrom err");
return -1;
}
// printf("client ip:%s ,port:%d buf:%s\n", inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port),msg.text);
printf("ip:%s 状态:chat 内容:%s\n", msg.name,msg.text);
}
`