一。并发服务器(多线程)实现
多线程实现服务器就是使用使用任务创建函数,即不停的判断是否有客户端连接服务器,如果发现有连接,就使用xTaskCreate创建任务(线程)对此进行处理。如果发送数据失败,就删除这个任务。如果内存占用满,会创建任务失败,即客户端连接服务器失败。
(1)socket_thread_server.h
#ifndef _SOCKET_THREAD_SERVER_H
#define _SOCKET_THREAD_SERVER_H
void vThreadServerTask(void);
#endif
2.socket_thread_server.c
#include "socket_tcp_server.h"
#include "socket_wrap.h"
#include "FreeRTOS.h"
#include "task.h"
#include "ctype.h"
#include "socket_thread_server.h"
#include "cmsis_os.h"
static char ReadBuff[BUFF_SIZE];
这个是创建的线程的主函数,即那个任务的for(;;)内容
void vNewClientTask(void const* argument){
int cfd=*(int *)argument;
int n,i;
while(1){
n=Read(cfd,ReadBuff,BUFF_SIZE);
if(n<=0){
close(cfd);
vTaskDelete(NULL);
}
for(i=0;i<n;i++){
ReadBuff[i]=toupper(ReadBuff[i]);
}
n=Write(cfd,ReadBuff,n);
if(n<0){
close(cfd);
vTaskDelete(NULL);
}
}
}
这个函数为线程创建
void vThreadServerTask(void){
int sfd, cfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
//创建socket
sfd=Socket(AF_INET, SOCK_STREAM, 0);
//绑定socket
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
Bind(sfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//监听socket
Listen(sfd,5);
//等待客户端连接
client_addr_len=sizeof(client_addr);
while(1){
/*
#define NUM_SOCKETS MEMP_NUM_NETCONN
每创建socket lwip都会分配一片内存空间,在lwip内部有
很多宏,NUM_SOCKETS就定义 一共支持多少个socket,也就是说
能分配多少fd
#define MEMP_NUM_NETCONN 8
*/
cfd=Accept(sfd,(struct sockaddr *)&client_addr,&client_addr_len);
printf("client is connect cfd = %d\r\n",cfd);
if(xTaskCreate( (TaskFunction_t) vNewClientTask,
"Client",
128,//1k
(void *)&cfd,
osPriorityNormal,
NULL) != pdPASS){
printf("create task fail!\r\n");
}
}
}
结果:
二。多路IO服用服务器模型
三。select API
1.标准select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
//readfds: 监控有读数据到达文件描述符集合,传入传出参数
//writefds: 监控写数据到达文件描述符集合,传入传出参数
//exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
//timeout: 定时阻塞监控时间,3种情况
//1.NULL,永远等下去
//2.设置timeval,等待固定时间
//3.设置timeval里时间均为0,检查描述字后立即返回,轮询
//return: 成功:所监听的所有监听集合中,满足条件的总数!
//失败:0 超时
//错误:-1
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
/*
注意:
1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
*/
2.LWIP select
/* FD_SET used for lwip_select */
#ifndef FD_SET
#undef FD_SETSIZE
/* Make FD_SETSIZE match NUM_SOCKETS in socket.c */
#define FD_SETSIZE MEMP_NUM_NETCONN
#define FDSETSAFESET(n, code) do { \
if (((n) - LWIP_SOCKET_OFFSET < MEMP_NUM_NETCONN) && (((int)(n) - LWIP_SOCKET_OFFSET) >= 0)) { \
code; }} while(0)
#define FDSETSAFEGET(n, code) (((n) - LWIP_SOCKET_OFFSET < MEMP_NUM_NETCONN) && (((int)(n) - LWIP_SOCKET_OFFSET) >= 0) ?\
(code) : 0)
#define FD_SET(n, p) FDSETSAFESET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] |= (1 << (((n)-LWIP_SOCKET_OFFSET) & 7)))
#define FD_CLR(n, p) FDSETSAFESET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] &= ~(1 << (((n)-LWIP_SOCKET_OFFSET) & 7)))
#define FD_ISSET(n,p) FDSETSAFEGET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] & (1 << (((n)-LWIP_SOCKET_OFFSET) & 7)))
#define FD_ZERO(p) memset((void*)(p), 0, sizeof(*(p)))
//fd_set
typedef struct fd_set
{
unsigned char fd_bits [(FD_SETSIZE+7)/8];
} fd_set;
//MEMP_NUM_NETCONN
#if !defined MEMP_NUM_NETCONN || defined __DOXYGEN__
#define MEMP_NUM_NETCONN 4
#endif
//LWIP_SOCKET_OFFSET
#if !defined LWIP_SOCKET_OFFSET || defined __DOXYGEN__
#define LWIP_SOCKET_OFFSET 0
#endif
3.select编程模型
四。并发服务器(select)实现
(1)socket_select_server.c
#include "socket_wrap.h"
#include "socket_select_server.h"
#include "socket_tcp_server.h"
#include "string.h"
#include "FreeRTOS.h"
#include "task.h"
#include "ctype.h"
static char ReadBuff[BUFF_SIZE];
/**
* @brief select 并发服务器
* @param none
* @retval none
*/
void vSelectServerTask(void)
{
int sfd, cfd, maxfd, i, nready, n, j;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
fd_set all_set, read_set;
//FD_SETSIZE里面包含了服务器的fd
int clientfds[FD_SETSIZE - 1];
//创建socket
sfd = Socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定socket
Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
//监听socket
Listen(sfd, 5);
client_addr_len = sizeof(client_addr);
//初始化 maxfd 等于 sfd
maxfd = sfd;
//清空fdset
FD_ZERO(&all_set);
//把sfd文件描述符添加到集合中
FD_SET(sfd, &all_set);
//初始化客户端fd的集合
for(i = 0; i < FD_SETSIZE -1 ; i++){
//初始化为-1
clientfds[i] = -1;
}
while(1)
{
//每次select返回之后,fd_set集合就会变化,再select时,就不能使用,
//所以我们要保存设置fd_set 和 读取的fd_set
read_set = all_set;
nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);
//没有超时机制,不会返回0
if(nready < 0){
printf("select error \r\n");
vTaskDelete(NULL);
}
//判断监听的套接字是否有数据
if(FD_ISSET(sfd, &read_set)){
//有了客户端进行连接了
//客户接入
cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
if(cfd < 0){
printf("accept socket error\r\n");
//继续select
continue;
}
printf("new client connect fd = %d\r\n", cfd);
//把新的cfd 添加到fd_set集合中
FD_SET(cfd, &all_set);
//更新要select的maxfd
maxfd = (cfd > maxfd)?cfd:maxfd;
//把新的cfd 保存到cfds集合中
for(i = 0; i < FD_SETSIZE -1 ; i++){
if(clientfds[i] == -1){
clientfds[i] = cfd;
//退出,不需要添加
break;
}
}
//没有其他套接字需要处理:这里防止重复工作,就不去执行其他任务
if(--nready == 0){
//继续select
continue;
}
}
//遍历所有的客户端文件描述符
for(i = 0; i < FD_SETSIZE -1 ; i++){
if(clientfds[i] == -1){
//继续遍历
continue;
}
//是否在我们fd_set集合里面
if(FD_ISSET(clientfds[i], &read_set)){
n = Read(clientfds[i], ReadBuff, BUFF_SIZE);
//Read函数已经关闭了这个客户端的fd
if(n <= 0){
//从集合里面清除
FD_CLR(clientfds[i], &all_set);
//当前的客户端fd 赋值为-1
clientfds[i] = -1;
}else{
//进行大小写转换
for(j = 0; j < n; j++){
ReadBuff[j] = toupper(ReadBuff[j]);
}
//写回客户端
n = Write(clientfds[i], ReadBuff, n);
if(n < 0){
//从集合里面清除
FD_CLR(clientfds[i], &all_set);
//当前的客户端fd 赋值为-1
clientfds[i] = -1;
}
}
}
}
}
}
(1)socket_select_server.h
#ifndef _SOCKET_SELECT_SERVER_H
#define _SOCKET_SELECT_SERVER_H
void vSelectServerTask(void);
#endif
结果:
1.什么是IO复用?
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。
2.IO复用原理:
我认为是通过创建1个任务(即select)来检测多个进程的读取,不需要每个任务都检测自己是否读取。这样减少系统的开销,节省空间(即系统不需要创建特定的任务来检查是否有读取过来,与DMA似乎有一样的功能),缺点是编程变的复杂。
综上所述,
(1)多线程就是普通的创建方法,在普通创建1个服务器的情况下,使用一个while循环,不停地轮寻是否要accept
(2)io是优化版,不需要轮寻,让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以进行,达到时间复用的目的。