STM32+LWIP服务器实现多客户端连接

用过正点原子LWIP服务器例程开发的朋友可能知道,例程的设计是只支持一个客户端连接的,但实际应用中往往需要用到多客户端连接。下面是在正点原子扩展例程 网络实验14 NETCONN_TCP 服务器(UCOSIII版本) 的基础上进行修改,实现多客户端连接的一个方法。

1、TCP服务器创建过程

建立一个TCP服务器需要经过
创建连接
conn=netconn_new(NETCONN_TCP); //创建一个TCP链接

绑定端口号
netconn_bind(conn,IP_ADDR_ANY,TCP_SERVER_PORT); //绑定端口 8088号端口

监听
netconn_listen(conn); //进入监听模式

接收连接请求
err = netconn_accept(conn,&newconn); //接收连接请求

等步骤。而例程里将这些步骤都放在了同一个任务线程里面去操作,一旦接收到连接就进入while (1) 死循环里传输数据,这当然会限制连接。要实现多客户端连接,那就得把这些步骤分开来操作。

2、多客户端连接设计思路

我们可以分成三个任务线程来实现多客户端的连接。第一个用来创建TCP服务器以及监听接收连接请求;第二个用来处理连接成功之后的数据传输以及断开连接等操作;另外一个任务专门用来创建第二个任务线程,这是为了方便内存以及连接数量和状态的管理。

3、实现代码

首先定义一个结构体方便管理客户端

#define CLIENTMAX   20 //最大客户端连接数量
//客户端任务结构体
typedef struct  
{
	struct netconn *conn;//客户端(连接结构体)
	OS_TCB    *clientTCB;//客户端(任务控制块)
	CPU_STK   *clientSTK;//客户端(任务堆栈)
	u8               num;//客户端(编号)
}tcp_client;
//客户端地址结构体
typedef struct  
{
	tcp_client  *client[CLIENTMAX];//客户端连接地址保存(最多20个客户端连接  地址全存这里了)
	u8            state[CLIENTMAX];//客户端连接状态
}client_ad;
client_ad                 clientad; //TCP客户端地址结构体(全局变量)

然后是第一个任务线程:创建TCP服务器

void svr_task(void *arg)
{
	u8 port=8888;
	OS_ERR         oserr;                       //错误标志(UCOSIII)
	struct netconn *conn,*newconn;              //定义TCP服务器连接 与 新连接	
	conn = netconn_new(NETCONN_TCP);            //创建一个TCP服务器连接
//	conn->pcb.tcp->so_options |= SOF_KEEPALIVE;  //保活检测机制用于检测异常断线(非必要,可注释)	
	netconn_bind(conn,IP_ADDR_ANY,port);         //绑定端口号	
	netconn_listen(conn);  		                 //进入监听模式	
	conn->recv_timeout = 5;  	                 //禁止阻塞线程 等待5ms
	while(1)
	{
			if(netconn_accept(conn,&newconn) == ERR_OK) //接收到正确的连接请求
			{		
				newconn->recv_timeout = 5;                //禁止阻塞线程 等待5ms	
				if(client_init((void *)newconn) != ERR_OK)//判断 TCP 客户端任务是否创建成功
				{                                         //若创建失败
					netconn_close(newconn);                 //关闭 TCP client 连接
					netconn_delete(newconn);                //删除 TCP client 连接
				}
			}
		OSTimeDlyHMSM(0,0,0,5,0,&oserr);                      //延时 5ms (任务切换)
	}
}

第二个任务线程:创建客户端 client_init()

err_t client_init(void *arg)
{
	u8          clientnum;                             //用于确定几号 TCP client 需要创建
	OS_ERR          err;                             //错误标志(UCOSIII)
	tcp_client    *client;                             //新建TCP client 连接结构体
	CPU_SR_ALLOC();                                    //定义一个局部变量
	
	client=(tcp_client*)mymalloc(SRAMDTCM,16);          //给设备结构体分配空间(16字节)
	if(client == NULL) return ERR_MEM;                 //分配空间失败 返回内存错误
	client->conn = (struct netconn *)arg;              //分配空间成功 将传递进入的入口参数给设备结构体
	client->clientTCB=(OS_TCB *)mymalloc(SRAMDTCM,196); //给设备结构体内任务控制块分配空间(196字节)
	if(client->clientTCB == NULL)
	{                                                  //给设备结构体内任务控制块分配空间失败
		myfree(SRAMDTCM, (void * )client);                //释放已分配的设备结构体空间
		return ERR_MEM;                                  //返回内存错误
	}
	client->clientSTK=(CPU_STK*)mymalloc(SRAMDTCM,1024);//给设备结构体内任务堆栈分配空间(1024字节)
	if(client->clientSTK == NULL)
	{                                                  //给设备结构体内任务堆栈分配空间失败
		myfree(SRAMDTCM, (void * )(client->clientTCB));   //释放已分配的设备结构体内任务控制块空间
		myfree(SRAMDTCM, (void * )client);	               //释放已分配的设备结构体空间
		return ERR_MEM;                                  //返回内存错误
	}
	for(clientnum=1;clientnum<CLIENTMAX;clientnum++)   //循环检测客户端连接状态(定义最多20个客户端)
	{
		if(clientad.state[clientnum]==0)                 //检测到第 clientnum 个客户端连接状态为0(未连接)
		{			
			client->num=clientnum;                         //给设备编号 clientnum
			clientad.client[clientnum]=client;             //保存设备地址
			break;                                         //完成操作 跳出检测  开始创建设备任务
		}
	}		
	CPU_CRITICAL_ENTER();	                             //进入临界区(代码保护 关中断)

	//创建TCP客户端任务
	OSTaskCreate((OS_TCB 	* )(client->clientTCB),		
				 (CPU_CHAR	* )"tcp_Server task", 		
								 (OS_TASK_PTR )tcp_server_thread, 			
								 (void		* )client,					
								 (OS_PRIO	  )10,     //9+clientnum
								 (CPU_STK   * )(client->clientSTK),	
								 (CPU_STK_SIZE)256/10,	
								 (CPU_STK_SIZE)256,		
								 (OS_MSG_QTY  )0,					
								 (OS_TICK	  )0,					
								 (void   	* )0,					
								 (OS_OPT      )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
										 (OS_ERR 	* )&err);

	CPU_CRITICAL_EXIT();	                             //退出临界区(开中断)
	client_count++;
	if(err != OS_ERR_NONE)
	{                                                  //创建任务失败
		client_count--;
//		printf("%d号客户端任务创建失败\r\n",client->num);//打印信息
		myfree(SRAMDTCM, (void * )(client->clientTCB));   //释放已分配的设备结构体内任务控制块空间
		myfree(SRAMDTCM, (void * )(client->clientSTK));	 //释放已分配的设备结构体内任务堆栈空间
		myfree(SRAMDTCM, (void * )client);	               //释放已分配的设备结构体空间		
		return ERR_RTE;                                  //返回错误信息
	}
	clientad.state[clientnum]=1;                       //第 clientnum 个客户端连接状态置1(已连接)
	return ERR_OK;                                     //返回错误信息
}

这个任务开头部分可能看着有些复杂,但其实就是给一个变量申请内存以及申请失败的一些处理,应该还是不难理解的。

最后一个任务线程:客户端线程

static void tcp_server_thread(void *arg)
{
	u8 *tcp_server_recvbuf;	
	u8 *tcp_server_sendbuf;	
	CPU_SR_ALLOC();
	u8 online_check=0;
	u32 readval;
	OS_ERR       oserr; //错误标志(UCOSIII)
	u32 data_len = 0;	
	static u16_t 			port;
	struct pbuf *q;
	err_t recv_err;
	static ip_addr_t ip;
	tcp_client *client = (tcp_client *)arg;                 //将传递进入的入口参数给设备结构体
	struct netbuf *recvbuf;
	netconn_getaddr(client->conn,&ip,&port,0);              //获取远端IP地址和端口号
	printf("%d号客户端 %d.%d.%d.%d 端口号 %d 已连接\r\n",   //打印信息
					client->num,(u8)(ip.addr>>0),(u8)(ip.addr>>8),     //打印信息
					(u8)(ip.addr>>16),(u8)(ip.addr>>24),port);     //打印信息		
	keep_alive=0;	
	tcp_server_recvbuf=mymalloc(SRAMIN,TCP_SERVER_RX_BUFSIZE);	//TCP客户端接收数据缓冲区[TCP_SERVER_RX_BUFSIZE]
	tcp_server_sendbuf=mymalloc(SRAMIN,TCP_SERVER_TX_BUFSIZE);
	while(1)
	{		
		if((recv_err = netconn_recv(client->conn,&recvbuf)) == ERR_OK)  	//接收到数据
		{								
				OS_CRITICAL_ENTER(); //关中断
				memset(tcp_server_recvbuf,0,TCP_SERVER_RX_BUFSIZE);  //数据接收缓冲区清零
				data_len = 0;
				for(q=recvbuf->p;q!=NULL;q=q->next)  //遍历完整个pbuf链表
				{
					//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
					//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
					if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
					else memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
					data_len += q->len;  	
					if(data_len > TCP_SERVER_RX_BUFSIZE) break; //超出TCP客户端接收数组,跳出	
				}
				OS_CRITICAL_EXIT();  //开中断		
				recv_err = netconn_write(client->conn ,tcp_server_sendbuf,data_len,NETCONN_COPY); //发送tcp_server_sendbuf中的数据
				data_len=0;  //复制完成后data_len要清零。
				netbuf_delete(recvbuf);					
		}
		else if(recv_err == ERR_CLSD||recv_err==ERR_RST)  //关闭连接或复位数据
			break;
//		if(LAN8720_STATUS_LINK_DOWN == LAN8720_GetLinkState())	//网线未连接
//				online_check = 1;
		LAN8720_ReadPHY(LAN8720_BSR,&readval); //获取连接状态(硬件,网线的连接,不是TCP、UDP等软件连接!) 
	    if((readval&LAN8720_BSR_LINK_STATUS)==0)
	        online_check = 1;
		else
			online_check = 0;
		if(online_check) 
			break;
	}
	if(clientad.state[client->num]==1)
	{
			client_count--;		
			netconn_close(client->conn); 
			netconn_delete(client->conn);                           //删除连接
			clientad.state[client->num]=0;                          //第 client->num 个设备状态置0 表示客户端未连接
			printf("%d号客户端 %d.%d.%d.%d 端口号 %d 已断开\r\n",   //打印信息
						 client->num,(u8)(ip.addr>>0),(u8)(ip.addr>>8),   //打印信息
								 (u8)(ip.addr>>16),(u8)(ip.addr>>24),port);   //打印信息
			myfree(SRAMIN, tcp_server_recvbuf);
			myfree(SRAMIN, tcp_server_sendbuf);
			myfree(SRAMDTCM, (void * )(client->clientTCB));          //释放已分配的设备结构体内任务控制块空间
			myfree(SRAMDTCM, (void * )(client->clientSTK));				  //释放已分配的设备结构体内任务堆栈空间
			myfree(SRAMDTCM, (void * )client);	                      //释放已分配的设备结构体空间			
			OSTaskDel(NULL,&oserr);                                 //删除当前设备任务
	}
}

数据的收发和处理都在这个线程里操作,接收到一个连接请求就创建一个客户端连接,每个线程之间相互独立,不相干扰!断开连接也能及时释放内存!
整理了一个Demo,有需要的可以自行下载

  • 14
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 38
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值