用过正点原子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,有需要的可以自行下载!