网络编程day04(UDP、Linux IO 模型)
目录
【1】UDP
1》通信流程
2》函数接口
1> recvfrom
2> sendto
3》代码展示
1> 服务器代码
2> 客户端代码
【2】Linux IO 模型
场景假设一
1》阻塞式IO:最常见、效率低、不耗费CPU
2》 非阻塞 IO:轮询、耗费CPU,可以处理多路IO
设置非阻塞的方式
1> 通过函数自带参数设置
2> 通过设置文件描述符的属性,把文件描述符的属性设置为非阻塞
3》信号驱动IO/异步IO:异步通知方式,需要底层驱动的支持
4》三种模型对比
【1】UDP
1》通信流程
服务器----------------------------------------------------------------------------》短信的接收方
- 创建数据报套接字(socket)------------------》有手机
- 指定网络信息--------------------------------------》有号码
- 绑定套接字(bind)------------------------------》绑定手机
- 接收、发送消息(recvfrom sendto)-------》收短信
- 关闭套接字(close)----------------------------》接收完毕
客户端---------------------------------------------------------------------------》短信的发送方
- 创建数据报套接字(socket)------------------》有手机
- 指定网络信息--------------------------------------》有对方号码
- 接收、发送消息(recvfrom sendto)-------》发短信
- 关闭套接字(close)----------------------------》发送完毕
2》函数接口
1> recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd:套接字描述符
buf:接收缓存区的首地址
len:接收缓存区的大小
flags:0
src_addr:发送端的网络信息结构体的指针
addrlen:发送端的网络信息结构体的大小的指针
返回值:
成功接收的字节个数
失败:-1
0:客户端退出
2> sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
sockfd:套接字描述符
buf:发送缓存区的首地址
len:发送缓存区的大小
flags:0
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小
返回值:
成功发送的字节个数
失败:-1
3》代码展示
1> 服务器代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret;
// 1.创建数据报套接字(socket)------------------》有手机
int 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, caddr;
saddr.sin_family = AF_I
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(caddr);
// 3.绑定套接字(bind)------------------------------》绑定手机
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind okk\n");
// 4.接收、发送消息(recvfrom sendto)-------》收短信
while (1)
{
// 最后两个参数存放:发送消息的人的信息
ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
if (ret < 0)
{
perror("recvfrom err");
return -1;
}
else
{
printf("ip:%s port:%d buf:%s\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port), buf);
memset(buf, 0, sizeof(buf));
}
}
// 5.关闭套接字(close)----------------------------》接收完毕
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>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret;
// 1.创建数据报套接字(socket)------------------》有手机
int 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, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("192.168.253.145");
int len = sizeof(caddr);
// 4.接收、发送消息(recvfrom sendto)-------》收短信
while (1)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
if (strcmp(buf, "quit") == 0)
{
break;
}
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, sizeof(saddr));
memset(buf,0,sizeof(buf));
}
// 5.关闭套接字(close)----------------------------》接收完毕
close(sockfd);
return 0;
}
注意:
1.对于TCP是先运行服务器,客户端才能运行
2.对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接的,所以服务器和客户端谁先开始,没有关系
3.一个服务器可以同时连接多个客户端,想知道是哪个客户端登录,可以在服务器代码里加上打印IP和端口号的代码
4.UDP,客户端当使用send的时候,上面要加上connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据,这样就不需要使用sendto而是用send就可以了
5.在TCP里,也可以使用recvfrom和sendto,使用时将后面的两个参数都写为NULL即OK
【2】Linux IO 模型
4种:阻塞IO、非阻塞IO、信号驱动IO(异步IO)、IO多路复用
场景假设一
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
- 一直看着他,一直在一个房间呆着:不累,但是不能处理其他的事情
- 时不时的进房间看看:累,但是可以处理其他事情
- 睡觉,听孩子哭不哭:互不耽误
1》阻塞式IO:最常见、效率低、不耗费CPU
阻塞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粘包
tcp拆包
TCP粘包、拆包发生原因:
发生TCP粘包或拆包有很多原因,常见的一下几点:
1.要发送的数据大于TCP发送缓存区剩余空间大小,将发生拆包
2.待发送数据大于MSS(传输层的最大报文长度),将进行拆包(到网络层拆包-idipflags)
3.要发送的数据小于TCP发送缓存区的大小,TCP将多次写入缓存区的数据一次发送出去,将会发生粘包
4.接收数据端的应用层没有及时读取缓冲区中的数据,将发生粘包
粘包解决方法:解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1.发送端给每个数据包添加首部,首部中应该至少包含数据包的长度,这样接收端在接受到数据后,通过读取包首部的长度字段,便可以知道每一个数据报的实际长度了
2.发送端将每个数据包封装为固定长度,这样接收端每次从接受缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3.可以再数据包之间设置边界,如添加特殊符号,这样接收端通过这个边界既可以将不同的数据包拆分开来。
4.延时发送
2》 非阻塞 IO:轮询、耗费CPU,可以处理多路IO
•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
设置非阻塞的方式
1> 通过函数自带参数设置
2> 通过设置文件描述符的属性,把文件描述符的属性设置为非阻塞
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符属性
参数:
fd:文件描述符
cmd:设置方式 - 功能选择
F_GETFL 获取文件描述符的状态信息 第三个参数化忽略
F_SETFL 设置文件描述符的状态信息 通过第三个参数设置
O_NONBLOCK 非阻塞
O_ASYNC 异步
O_SYNC 同步
arg:设置的值 in
返回值:
特殊选择返回特殊值 - F_GETFL 返回的状态值(int)
其他:成功0 失败-1,更新errno
使用:0为例
0-原本:阻塞、读权限 修改或添加非阻塞
int flags=fcntl(0,F_GETFL);//1.获取文件描述符原有的属性信息
flags = flags | O_NONBLOCK;//2.修改添加权限
fcntl(0,F_SETFL,flags); //3.将修改好的权限设置回去
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//1.获取文件描述符的原有属性
int flags = fcntl(0,F_GETFL);
//2.修改文件描述符的属性
flags = flags | O_NONBLOCK;
//3.设置文件描述符的属性
fcntl(0,F_SETFL,flags);
char buf[32];
while(1)
{
if(fgets(buf,sizeof(buf),stdin) == NULL)
{
perror("err\n");
}
else
{
printf("buf: %s\n",buf);
}
sleep(1);
}
return 0;
}
3》信号驱动IO/异步IO:异步通知方式,需要底层驱动的支持
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。确认发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知
1.通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO
2.应用程序收到信号后做异步处理即可
应用程序需要把自己的进程号告诉内核,并打开异步通知机制
//1.设置将文件描述符和进程号提交给内核驱动
//一旦fd有事件响应, 则内核驱动会给进程号发送一个SIGIO的信号
fcntl(fd,F_SETOWN,getpid());
//2.设置异步通知
int flags;
flags = fcntl(fd, F_GETFL); //获取原属性
flags |= O_ASYNC; //给flags设置异步 O_ASUNC 通知
fcntl(fd, F_SETFL, flags); //修改的属性设置进去,此时fd属于异步
//3.signal捕捉SIGIO信号 --- SIGIO:内核通知会进程有新的IO信号可用
//一旦内核给进程发送sigio信号,则执行handler
signal(SIGIO,handler);
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
int fd;
void handler(int sig)
{
char buf[32];
printf("------------------------\n");
read(fd, buf, sizeof(buf));
printf("mouse: %s\n", buf);
}
int main(int argc, char const *argv[])
{
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("fd err\n");
return -1;
}
// 1.将进程号和文件描述符交给内核
fcntl(fd, __F_SETOWN, getpid());
// 2.设置异步通知
int flags = fcntl(fd, F_GETFL);
flags = flags | O_ASYNC;
fcntl(fd, F_SETFL, flags);
// 3.捕捉信号,做逻辑处理
signal(SIGIO, handler);
while (1)
{
printf("111\n");
sleep(1);
}
return 0;
}
4》三种模型对比
阻塞 IO(Blocking IO) | 非阻塞 IO(Non-blocking IO) | 信号驱动 IO(Signal-driven IO) | |
同步性 | 同步 | 非同步 | 异步 |
描述 | 调用IO操作的线程会被阻塞,直到操作完成 | 调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作 | 当IO操作可以进行时,内核会发送信号,通知进程 |
特点 | 最常见、效率低、不耗费CPU | 轮询、耗费CPU,可以处理多路IO,效率高 | 异步通知方式,需要底层驱动的支持 |
适应场景 | 小规模IO操作,对性能要求不高 | 高并发网络服务器,减少线程阻塞时间 | 实时性要求高的应用,避免轮询开销 |
今天的分享就到这里结束啦,如果有哪里写的不好的地方,请指正。
如果觉得不错并且对你有帮助的话点个关注支持一下吧!