LWIP学习之Socket(应用篇)
原文链接:https://blog.csdn.net/qq_39854159/article/details/120940325
Socket接口编程
https://blog.csdn.net/qq_39854159/article/details/120693512?spm=1001.2014.3001.5501
1 客户端和服务器流程图
其流程图就像打客服电话一样,服务器可以理解成中国移动客服电话一样,我们便是客户端。
服务器就需要使用Socket()注册一个电话,使用bind()绑定一个电话号码10086,listen()就是一个客服在监听着电话。然后进入accept()阻塞之中。毕竟这个时候没人打电话也不需要处理什么东西。
这个时候我们客户端也可以使用socket()注册一个电话,然后使用connect()拨打10086。
原本闲着没事的客服听到叮铃铃电话响了,就会从accept()的阻塞态跳出,期间会进行著名的TCP三握手。经过“喂~” “喂?” “喂~”之后,就可以投诉为什么给我张套餐费用巴拉巴拉……。然后10086给出他的解释。
客户端使用close()函数怒挂了电话。通过四次挥手之后,10086也使用close()挂上了电话。
代码分析:
若对以上流程图用到的基本socket API有不懂的可以看下我的另外一篇文章《Socket API篇》
这里代码就使用野火的《LwIP应用开发实战指南》中的例程。
2 服务器
#include "tcpecho.h"
#include "lwip/opt.h"
#if LWIP_SOCKET
#include <lwip/sockets.h>
#include "lwip/sys.h"
#include "lwip/api.h"
/*--------------------------------------------------------------------*/
#define PORT 5001
#define RECV_DATA (1024)
static void
tcpecho_thread(void *arg)
{
int sock = -1,connected;
char *recv_data;
struct sockaddr_in server_addr,client_addr;
socklen_t sin_size;
int recv_data_len;
recv_data = (char *)pvPortMalloc(RECV_DATA); //申请1024字节内存,作为接收数据的存储区
if (recv_data == NULL)
{
printf("No memory\n"); //申请失败跳转到__exit
goto __exit;
}
sock = socket(AF_INET, SOCK_STREAM, 0); //创建一个socket套接字sock
if (sock < 0)
{
printf("Socket error\n"); //创建失败跳转到__exit
goto __exit;
}
server_addr.sin_family = AF_INET; //选择IPV4协议族
server_addr.sin_addr.s_addr = INADDR_ANY;//允许任何地址链接
server_addr.sin_port = htons(PORT); //设置端口号5001
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)//使用bind()绑定之前配置的信息
{
printf("Unable to bind\n");
goto __exit;
}
if (listen(sock, 5) == -1)//sock最多监听5个链接
{
printf("Listen error\n");
goto __exit;
}
while (1)
{
sin_size = sizeof(struct sockaddr_in);
connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);//阻塞等待客户端链接,若没有链接任务会在此进入一个阻塞态。
printf("new client connected from (%s, %d)\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));//打印客户端的地址端口信息
{
int flag = 1;
setsockopt(connected,
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(void *) &flag, /* the cast is historical cruft */
sizeof(int)); /* length of option value */
}
while (1)
{
recv_data_len = recv(connected, recv_data, RECV_DATA, 0);
//客户端连接上后会在此阻塞,等待接收数据
if (recv_data_len <= 0)
//若客户端断开连接,客户端发送FIN包,recv()会返回-1,跳出这个while循环回到accept()阻塞
break;
printf("recv %d len data\n",recv_data_len);
write(connected,recv_data,recv_data_len);//写数据
}
if (connected >= 0)
closesocket(connected);//若之前有客户的连接,这里会关闭客户端连接
connected = -1;
}
__exit:
if (sock >= 0) closesocket(sock);//服务器socket创建成功,则关闭服务器socket
if (recv_data) free(recv_data);//若接收内存申请成功,则释放接收内存
}
void
tcpecho_init(void)
{
sys_thread_new("tcpecho_thread", tcpecho_thread, NULL, 512, 4);
}
3 客户端
#include "client.h"
#include "lwip/opt.h"
#include "lwip/sys.h"
#include "lwip/api.h"
#include <lwip/sockets.h>
#define PORT 5001
#define IP_ADDR "192.168.0.181"
static void client(void *thread_param)
{
int sock = -1;
struct sockaddr_in client_addr;
uint8_t send_buf[]= "This is a TCP Client test...\n";
while (1)
{
sock = socket(AF_INET, SOCK_STREAM, 0);//创建一个客户端socket叫sock
if (sock < 0)
{//若创建失败延时10ms继续创建
printf("Socket error\n");
vTaskDelay(10);
continue;
}
client_addr.sin_family = AF_INET;//服务器设置的地址协议族IPV4
client_addr.sin_port = htons(PORT);//服务器设置的端口号
client_addr.sin_addr.s_addr = inet_addr(IP_ADDR);//服务器的地址
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if (connect(sock,
(struct sockaddr *)&client_addr,
sizeof(struct sockaddr)) == -1)//连接服务器
{//若连接失败则关闭socket()延时10ms跳出while(1)
printf("Connect failed!\n");
closesocket(sock);
vTaskDelay(10);
continue;
}
printf("Connect to iperf server successful!\n");
while (1)
{
if (write(sock,send_buf,sizeof(send_buf)) < 0)//写数据
break;
vTaskDelay(1000);
}
closesocket(sock);//关闭套接字
}
}
void
client_init(void)
{
sys_thread_new("client", client, NULL, 512, 4);
}
4 不足之处
以上是野火提供的socket的用法是重复型服务器,一次只能处理一个连接。当其他客户端使用connect()连接之后,若客户端不close(),那么服务器便会阻塞在recv()函数处。正确客户端连接便不会被服务器响应。
5 解决思路
使用select()函数。在recv()之前使用select()先检测是否正确的连接到来。
select()函数的返回值
当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修 改文件描述符集,并返回一个大于0 的数。
当没有满足条件的文件描述符,且设置的timeval 监控时间超时时,select函数会返回一个为0的值。
当select返回负值时,发生错误。
现在FD队列中添加需要监控的客户端,如按键和com端口。
例子参考:
FD_ZERO(&rds);
FD_SET(button_fd,&rds); //添加按键fd
FD_SET(com,&rds)//添加com fd
ret=select(fd+1,&rds,NULL,NULL,NULL);
if(ret<0)
{
printf("select failure\n");
exit(1);
}
if(ret==0)
{
printf("select timeout\n");
}
else if(FD_ISSET(button_fd,&rds)) //按键客户端连接
{
read(button_fd,&but_status,sizeof(but_status));
}
else if(FD_ISSET(com_fd,&rds)) //com客户端连接
{
read(.......);
}