7.2 网络编程 day3
UDP编程
通信流程 — 无连接(connect accept)的过程
UDP 无法判断客户端是否退出:
使用心跳包: 使用客户端, 定时给服务器发送内容
udp流程:(类似发短信)
server:
创建数据报套接字(socket(,SOCK_DGRAM,))----->有手机
绑定网络信息(bind())-----------> 绑定IP和port(发短信知道发给谁)
接收信息(recvfrom())------------>接收信息,同时可以获取到发送者的IP和port
关闭套接字(close())-------------->结束
client:
创建数据报套接字(socket())----------------------->有手机
指定服务器的网络信息------------------------------>有对方号码
发送信息(sendto())---------------------------->发送短信,根据填充的结构体信息
关闭套接字(close())---------------------------> 结束
函数接口
recvfrom
**./server**
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//1.创建数据报套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("sockfd is err:");
return -1;
}
//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("0.0.0.0");
int len = sizeof(caddr);
//绑定自己的ip和port
if(bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr))<0)
{
perror("bind is err:");
return -1;
}
char buf[128];
while(1)
{
//接受对方发送的内容
int recvbyte = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&caddr,&len);
if(recvbyte < 0)
{
perror("recvfrom is err:");
return -1;
}
else
{
printf("ip: %s, port: %d, %s\n",inet_ntoa(caddr.sin_addr),\
ntohs(caddr.sin_port),buf);
}
}
close(sockfd);
return 0;
}
sendto
**./client:**
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char const *argv[])
{
//1.创建数据报套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socker is err:");
return -1;
}
//2. 填充 服务器结构体
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
caddr.sin_port = htons(atoi(argv[2]));
caddr.sin_addr.s_addr = inet_addr(argv[1]);
char buf[128];
while(1)
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
//结构体信息, 决定了sendto发送给谁
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&caddr,sizeof(caddr));
}
close(sockfd);
return 0;
}
注意:
1、对于TCP是先运行服务器,客户端才能运行。
2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、UDP一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
以下内容面试可能会问: 感兴趣可以自己测试一下
4、UDP,客户端当使用send的时候,上面需要加connect,,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
sendto(结构体信息) // 通过结构体信息, sendto知道发送给哪个IP和port
connect(sockfd, 结构体信息) //UDP 无连接 ,仅仅指的是 告诉send将数据发送给谁
send(sockfd);
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个结构体参数都写为NULL就OK。
connect(sockfd, 结构体信息)
sendto(sockfd,结构体信息NULL)
项目: UDP聊天室
项目要求:
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
问题思考:
l 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
l 有几种消息类型?
登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
l 服务器如何存储客户端的地址?
数据结构使用链表最为简单
链表节点结构体:
struct node{
struct sockaddr_in addr;
struct node *next;
};
消息对应的结构体(同一个协议)
typedef struct msg_t
{
int type;//L M Q
char name[32];//用户名
char text[128];//消息正文
}MSG_t;
如何同时处理发送和接收?
多进程或多线程处理多任务;
程序流程图:
客户端:
服务器:
Linux下四种模型的特点:
阻塞式IO 非阻塞式IO 信号驱动IO(了解) IO多路复用(帮助TCP实现并发)
1.阻塞式IO: 最简单, 最常用,效率低;
阻塞I/O 模式是最普遍使用的I/O 模式
Ÿ 系统默认状态,套接字建立后所处于的模式就是阻塞I/O 模式。
Ÿ 目前学习的读写函数中会发生阻塞相关函数如下:
· read、recv、recvfrom
读阻塞–》需要读缓冲区中有数据可读,读阻塞才会解除
· write, send
写阻塞–》阻塞就是写入数据时遇到缓冲区满了的情况,需要等待缓冲区有空间后才能继续写入, 所以写阻塞发生的情况比较少.
accept connect
需要注意的是使用UDP时,UDP没有发送缓存区 ,则sendto没有阻塞
1)原因:
udp通信无连接,且无发送缓冲区(不怕粘包),即sendto在UDP中没有发送缓冲区。
2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
·
udp与tcp缓存区 仅作为了解
UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。
丢包出现原因: 接收缓存区满 网络拥堵, 传输错误
相比之下,TCP是一种面向连接的传输协议,它需要保证数据的可靠性和顺序性。TCP有发送缓存区和接收缓存区, 如果发送频率过快, 且内容小于发送缓存区的大小 , 可能会导致多个数据的粘包。如果发送的数据大于发送缓存区, 可能会导致拆包。
UDP不会造成粘包和拆包, TCP不会造成丢包
UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将UDP的每个数据包分隔开, 不会造成粘包。
TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;
TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)
TCP粘包:
TCP拆包:
UDP丢包:
2. 非阻塞式IO :可以处理多路IO;需要轮询,大量浪费CPU资源
1.当一个应用程序使用了非阻塞模式的套接字,则它需要使用一个循环来不停的测试是否一个文件描述符有数据可读。
2.应用程序不停的测试,会占用大量的cpu资源 ,所以说一般不适用
fcntl设置文件描述符的属性
声明: int fcntl (int fd, int cmd, ...arg);
头文件: #include<fcntl.h> #include<unistd.h>
功能:设置文件描述符的属性
参数: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 信号(异步),以便进程能够及时处理这些事件。
arg:文件描述符的属性 --------------------同上参数
返回值: 特殊选择:根据功能选择返回 (int 类型)
其他: 成功0 失败: -1;
使用: int flag;
// 1.获取该文件描述符0 (标准输入) 的原属性 : 标准输入原本具有阻塞的功能
int flag = fcntl(0, F_GETFL); //获取文件描述符原有信息后,保存在flag变量内
//2.修改对应的位nonblock(非阻塞)
int flag |= O_NONBLOCK; ( flag = flag | O_NONBLOCK)
// 3. 将修改好的属性写回去 (0 标准输入 -- 阻塞 改为 非阻塞)
fcntl (0, F_SETFL, flag); //文件描述符 设置状态 添加的新属性
2.信号驱动IO - 非重点
特点:异步通知模式,需要底层驱动的支持
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。
- 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
2.应用程序收到信号后做异步处理即可。
3.应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
核心代码如下:
//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);
移动鼠标相应鼠标事件, 否则打印lunch
#include <stdio.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
int fd;
void handler(int arg)
{
char buf[128];
int ret = read(fd,buf,sizeof(buf));
buf[ret] = '\0';
printf("mouse: %s\n",buf);
}
//操作鼠标设备,打印鼠标设备的内容,否则打印hello
int main(int argc, const char *argv[])
{
//打开鼠标文件
fd = open("/dev/input/mouse0",O_RDONLY);
//2.将该进程的进程号告诉内核
fcntl(fd,F_SETOWN,getpid());
//设置fd的属性 - 异步
int flag = fcntl(fd,F_GETFL);
flag = flag | O_ASYNC;
fcntl(fd,F_SETFL,flag);
signal(SIGIO,handler);
while(1)
{
printf("lunch!!\n");
sleep(1);
}
return 0;
}
3. I/O多路复用 - 帮助TCP实现并发服务器
1.进程中若需要同时处理多路输入输出 ,在使用单进程和单线程的情况下, 可使用IO多路复用处理多个请求;
2.IO多路复用不需要创建新的进程和线程, 有效减少了系统的资源开销。
就比如服务员给50个顾客点餐,分两步:
顾客思考要吃什么(服务器等待客户端数据发送)
顾客想好了,开始点餐(服务器接收客户端数据)
要提高效率有几种方法?
- 安排50个服务员 (类似于多进程/多线程实现服务器连接多个客户端,太占用资源)
2.哪个顾客想好了吃啥, 那个顾客来柜台点菜 (类似IO多路复用机制实现并发服务器)
实现IO多路复用的方式: select poll epoll
基本流程是:
- 先构造一张有关文件描述符的表;
- 清空表
- 将你关心的文件描述符加入到这个表中;
- 调用select函数。
- 判断是哪一个或哪些文件描述符产生了事件(IO操作);
- 做对应的逻辑处理;
1) 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);
功能:监测是哪些文件描述符产生事件,阻塞等待产生.
参数:nfds: 监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)
readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合; //NULL 表示不关心
timeout: 设为NULL,等待直到某个文件描述符发生变化;
设为大于0的值,有描述符变化或超时时间到才返回。
超时时间检测:如果规定时间内未完成函数功能,返回一个超时的信息,我们可以根 据该信息设定相应需求;
返回值: <0 出错 >0 表示有事件产生;
如果设置了超时检测时间:&tv
<0 出错 >0 表示有事件产生; ==0 表示超时时间已到;
结构体如下:
struct timeval {
long tv_sec; 以秒为单位,指定等待时间
long tv_usec; 以毫秒为单位,指定等待时间
};
void FD_CLR(int fd, fd_set *set); //将set集合中的fd清除掉
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中产生了事件
void FD_SET(int fd, fd_set *set); //将fd加入到集合中
void FD_ZERO(fd_set *set); //清空集合
select特点:
1. 一个进程最多只能监听1024个文件描述符 (32位) [64位为 2048]
2. select被唤醒之后要重新轮询(0-1023)一遍驱动,效率低(消耗CPU资源)
3. select每次会清空未响应的文件描述符,每次都需要拷贝用户空间的表到内核空间,效率低,开销较大
(03G是用户态,3G4G是内核态,两个状态来回切换 拷贝是非常耗时,耗资源的)
关于切换: (了解就好)
在使用select函数进行IO多路复用时,需要创建一个fd_set类型的表来存放待监视的文件描述符。这个fd_set表是在用户态创建的。
在调用select函数之前,需要将用户态中的fd_set表的数据拷贝到内核态中,以便内核能够根据这个表来进行IO事件的监视。每次调用select函数时,都需要将用户态的fd_set表重新拷贝到内核态,内核态以更新IO事件的监视情况,
最后在select函数返回时,内核态需要将发生IO事件的文件描述符的内容传递给用户态。以便用户态能够知道哪些文件描述符发生了IO事件。
select机制(辅助理解):
1. 头文件检测1024个文件描述符 0-1023
2. 在select的表内0~2存储标准输入、标准输出、标准出错
3. 监测的最大文件描述个数为fd+1(如果fd = 3,则最大为 4) : //因为从0开始的
4. select只对置1的文件描述符感兴趣 假如事件产生,select检测时 , 产生的文件描述符会保持1,未产生事件的会置0;
5. select每次轮询都会清空表(置零的清空) //需要在select前备份临时表
练习: 输入鼠标事件响应鼠标文件 输入键盘事件响应键盘文件
//输入鼠标事件响应鼠标文件 输入键盘事件响应键盘文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
int main(int argc, char const *argv[])
{
//打开鼠标
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);
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;
}
为什么不能 maxfd = accpetfd?
练习: 检测键盘和sockfd(TCP实现同时连接多个客户端 - 先不写通信)
./server:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
//select TCP_server 实现链接多个客户端
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socker is err:");
return -1;
}
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) <0)
{
perror("bind is err:");
return -1;
}
if(listen(sockfd,5) < 0)
{
perror("listen is err:");
return -1;
}
//1.创建表和备份表
fd_set readfds,tempfds;
//2.清空表
FD_ZERO(&readfds);
FD_ZERO(&tempfds);
//3.将关心的文件描述符添加到表内
FD_SET(0,&readfds);
FD_SET(sockfd,&readfds);
//将目前最大的文件描述符的下标赋值给maxfd
int maxfd = sockfd;
char buf[128];
while(1)
{
tempfds = readfds;
//4.检测表内文件描述符是否响应 maxfd+1代表当前文件描述服的数量
int ret = select(maxfd +1,&tempfds,NULL,NULL,NULL);
if(ret < 0)
{
perror("select is err:");
return -1;
}
//5.判断表内文件描述符是否有事件产生
//如果键盘输入有响应,则进行终端输入
if(FD_ISSET(0,&tempfds))
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
printf("key: %s\n",buf);
}
//如果建立连接的文件描述符产生响应,则应该与建立连接的客户端建立通信
if(FD_ISSET(sockfd,&tempfds))
{
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(acceptfd < 0)
{
perror("accept is err:");
return -1;
}
printf("fd: %d client : ip: %s port: %d\n",acceptfd,inet_ntoa(caddr.sin_addr),\
ntohs(caddr.sin_port));
//将新的通信的文件描述符加入到表内
FD_SET(acceptfd,&readfds);//maxfd
if(acceptfd > maxfd)
maxfd = acceptfd;
}
for(int i = 4; i <= maxfd; i++)
{
if(FD_ISSET(i,&tempfds))
{
int recvbyte = recv(i,buf,sizeof(buf),0);
if(recvbyte < 0)
{
perror("recv is err:");
return -1;
}
else if(recvbyte == 0)
{
printf("%d client is exit\n",i);
//关闭通信的文件描述符
close(i);
//把表内的相关文件描述服删除
FD_CLR(i,&readfds);
//maxfd表示最大文件描述符的下标
if(i == maxfd)
maxfd--;
}
else
{
printf("%d: %s\n",i,buf);
}
}
}
}
close(sockfd);
return 0;
}
作业:
- 掌握UDP和select相关代码;
- server给所有来连接的客户端发送消息(发群里)
- 实现: client_select_全双工(发群里)
- 努力去写 UDP聊天室(没有思路可以看聊天室文件内的视频)