嵌入式学习-网络-Day04
1.IO多路复用
1.1poll
poll同时检测键盘和鼠标事件
1.2epoll
2.服务器模型
2.1循环服务器模型
2.2并发服务器模型
多进程模型
多线程模型
IO多路复用模型
网络聊天室
项目要求
问题思考
程序流程图
1.IO多路复用
1.1poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
struct pollfd *fds
关心的文件描述符数组struct pollfd fds[N];
nfds:个数
timeout: 超时检测
毫秒级的:如果填1000,1秒
如果-1,阻塞 struct pollfd {
int fd; /* 检测的文件描述符 */
short events; /* 检测事件 */
short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */
};
事件: POLLIN :读事件
POLLOUT : 写事件
POLLERR:异常事件
流程 | select | poll |
1.建立一个文件描述符的表 | fd_set线性表 | struct pollfd fds[n]结构体数组 |
2.将关心的文件描述符加到表中 | FD_SET(fd,&readfds) | 结构体内容填充fds[m].fd= fd fds[m].events=POLLIN |
3. 然后调用一个函数。 select / poll 4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候 该函数才返回(阻塞)。 | select | poll |
5.判断 | FD_ISSET | revents==POLLIN |
6.相关操作 |
poll同时检测键盘和鼠标事件
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <poll.h>int main(int argc, char const *argv[])
{ // 打开文件
int fd_mouse = open("/dev/input/mouse0", O_RDONLY);
if (fd_mouse < 0)
{
perror("open err");
return -1;
} // 1.创建表,创建结构体数组
struct pollfd fds[100] = {};
// 2.添加关心的文件描述符
fds[0].fd = 0;
fds[0].events = POLLIN; fds[1].fd = fd_mouse;
fds[1].events = POLLIN; int last = 1;
char buf[128] = {};
// 3.循环调用poll
int ret;
while (1)
{
ret = poll(fds, last + 1, -1);
if (ret < 0)
{
perror("poll err");
return -1;
}
// 4.检测是哪一个文件描述符产生事件
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == 0) // 键盘事件
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
printf("key:%s\n", buf);
}
else if (fds[i].fd == fd_mouse)
{
ret = read(fd_mouse, buf, sizeof(buf) - 1);
buf[ret] = '\0';
printf("buf:%s\n", buf);
}
}
}
}
close(fd_mouse);
return 0;
}
特点:
1.优化文件描述符个数的限制;(根据poll函数第一个参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)
2.poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
3.poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
1.2epoll
特点:
- 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
- 异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
- epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
2.服务器模型
2.1循环服务器模型
同一时刻只能响应一个客户端的请求
socket();
bind();
listen();
while(1)
{
accept();
while(1)
{ process();//处理
}
close();
}
2.2并发服务器模型
同一时刻能响应多个客户端的请求,常用模型:多进程模型、多线程模型、IO多路复用
多进程模型
每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型。
伪代码:
socket();
bind();
listen();
while(1)
{
accept();if(fork()==0)//子进程{ while(1) {process();//处理}close();exit();}else{}}
多进程特点 :
- fork之前的代码被复制,但是不会重新执行一遍;fork之后的代码被复制,并且被再次执行一遍
- fork之后两个进程相互独立,子进程拷贝了父进程的所有代码,但是内存空间独立
- fork之前打开的文件,fork之后拿到的是同一个文件描述符,操作的是同一个文件指针
例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{
printf("hello world\n");
int a = 100;
int fd = open("./poll.c", O_RDONLY); pid_t pid = fork();
if(pid < 0 )
{
perror("fork err:");
return -1;
}
else if(pid == 0)
{
char buf[32]="";
a = 200;
printf("child:a = %d\n",a);
read(fd,buf,10);
printf("buf:%s\n",buf);
printf("childfd:%d\n",fd);
}
else
{
sleep(1);
char buf[32]="";
printf("father:a=%d\n",a);
read(fd,buf,10);
printf("fbuf:%s\n",buf);
printf("fatherfd:%d\n",fd);
}
printf("---------%d\n",a);
return 0;
}
注意:收到客户端消息后,打印下是来自哪个客户端的数据(来电显示)
使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#define N 128
void handler(int arg)
{
waitpid(-1,NULL,WNOHANG);
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s port\n", argv[0]);
return -1;
} // 1.创建套接字 协议族 类型 协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err:");
return -1;
}
// 2.填充结构体(ipv4)
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // 协议族ipv4
saddr.sin_port = htons(atoi(argv[1])); // 端口号(网络字节序)
// saddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址(网络字节序)
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); socklen_t len = sizeof(caddr);
// 3.绑定
int ret = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret < 0)
{
perror("bind err:");
return -1;
} // 4.监听
if (listen(sockfd, 5) < 0)
{
perror("listen err");
return -1;
} signal(SIGCHLD,handler); while (1)
{
// 5.等待连接
// int acceptfd = accept(sockfd, NULL, NULL);
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accpet err:");
return -1;
}
printf("acceptfd = %d\n", acceptfd);
printf("client ip :%s,port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pid_t pid =fork();
if(pid < 0)
{
perror("fork err");
return -1;
}
else if(pid == 0)
{
close(sockfd);
char buf[N]={};
int ret;
while(1)
{
ret = recv(acceptfd,buf,N,0);
if(ret < 0 )
{
perror("recv err");
return -1;
}
else if(ret == 0)
{
printf("client exit\n");
break;
}
else
{
printf("buf:%s\n",buf);
}
}
exit(-1);
}
close(acceptfd);
}
close(sockfd);
return 0;
}
多线程模型
每来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型:
伪代码:
socket();
bind();
listen()
while(1)
{
accept();
pthread_create();}
多线程实现并发服务器:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>#define N 128void * mythread(void * arg)
{
int acceptfd = *(int *)arg;
char buf[N]={};
int ret;
while(1)
{
ret = recv(acceptfd,buf,N,0);
if(ret < 0)
{
perror("recv err:");
break;
}
else if(ret == 0)
{
printf("client exit\n");
close(acceptfd);
break;
}
else
{
printf("%d said:%s\n",acceptfd,buf);
} }
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s port\n", argv[0]);
return -1;
} // 1.创建套接字 协议族 类型 协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err:");
return -1;
}
// 2.填充结构体(ipv4)
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // 协议族ipv4
saddr.sin_port = htons(atoi(argv[1])); // 端口号(网络字节序)
// saddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址(网络字节序)
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); socklen_t len = sizeof(caddr);
// 3.绑定
int ret = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret < 0)
{
perror("bind err:");
return -1;
} // 4.监听
if (listen(sockfd, 5) < 0)
{
perror("listen err");
return -1;
} while (1)
{
// 5.等待连接
// int acceptfd = accept(sockfd, NULL, NULL);
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accpet err:");
return -1;
}
printf("acceptfd = %d\n", acceptfd);
printf("client ip :%s,port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pthread_t tid; pthread_create(&tid,NULL,mythread,&acceptfd);
pthread_detach(tid);
}
close(sockfd);
return 0;
}
IO多路复用模型
借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起来稍显繁琐。
网络聊天室
项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
问题思考
- 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
- 有几种消息类型?
登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
- 服务器如何存储客户端的地址?
链表
链表节点结构体:
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;int memcmp(void *s1,void *s2,int size)
功能:比较两个空间内的值是否完全相同
- 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
程序流程图
客户端
伪代码:
服务器端://1.创建服务器流程//2.创建空链表//3.循环接受消息//根据消息类型调用函数login:
1.将登录信息发送给所有已经登录的客户端(链表,sockfd,msg)
2.将新登录的客户端插入链表(caddr)quit:
1.将谁退出的信息发送给所有登录的客户端(遍历链表)
2.将退出的客户端信息删除chat:
将聊天的内容转发给已经登录的客户端客户端:
1.客户端创建流程
2.登录(输入名字,发送给服务器)login
3.创建子进程(循环接收服务器的信息并打印)chat
4.父进程
终端输入信息并发送给服务器(注意发送的是否为quit,区分正常消息和退出消息)