这个只是一个简单的例子,主要是为了讲解socket的开发流程,在实际的开发的过程中代码会比这个例程复杂很多。
环境:
单片机:esp-idf
安卓手机:socket 调试工具
socket原理介绍
1.网络协议
在介绍socket之前先看看传说中的OSI网络七层协议
每层的意义只要了解就可以了,具体应用的时候在去深入研究某一个协议即可。
在说说TCP/IP协议,TCP/IP传输协议即传输控制/网络协议,也叫作网络通讯协议。它是在网络的使用中的最基本的通信协议。TCP/IP传输协议对互联网中各部分进行通信的标准和方法进行了规定.TCP/IP传输协议是严格来说是一个四层的体系结构,应用层、传输层、网络层和数据链路层都包含其中.它与前面说的七层结构的关系如下:
2.LwIP
LwIP是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。它只需十几KB的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端的嵌入式设备中使用。
乐鑫官方已经在esp-idf中移植好了lwip,用户在基于esp-idf开发时只需要按照通用的socket开发接口调用函数即可完成socket网络套接字编程。
3. socket网络编程概念
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
熟悉Linux编程的人可以比较好理解socket编程,socket可以理解成是一个文件,用户通过open,read,write 等函数操作这个文件,进行数据的读写操作。只不过这个文件在打开之前需要看着固定格式设置(ip和端口)才能作为网络文件被用户读写。
4. socket具体操作
socket编程主要分为tcp和udp ,它们有什么区别请自行百度。tcp可以分为server(服务器)和client(客户端)。两者通信的过程和主要函数如下:
函数说明:
服务器:
socket() 创建socket(套接字)文件句柄。
bind() 为这个socket绑定ip地址和端口
listen() 开始监听端口
accept() 等待客户端连接
read()/write() 双方开始通信。read()读取客户端的数据。write向客户端发送数据。
(在Lwip中发送和接收数据使用recv()/send())
close()关闭此socket
客户端:
唯一不同的就是多了一个connet()函数,这个函数的作用是通过预先给出的ip地址和端口连接到服务器。
源码分析
代码位置:/esp-idf/examples/protocols/sockets/tcp_server/ 主要代码为:
/**
* IPV4 和 IPV6 的配置,默认使用IPV4,对于应用开发来说没有区别,
* 我们只需在应用时设置不同的配置即可.
*/
#ifdef CONFIG_EXAMPLE_IPV4
struct sockaddr_in destAddr;//socket 结构体,定义了ipv4的一些配置,
destAddr.sin_addr.s_addr = htonl(INADDR_ANY);//目的ip地址为0.0.0.0,任何地址都可以连接此服务器
/*此参数表示套接字要使用的协议簇,
*一般设置为AF_INET表示TCP/IPV4
*AF_INET6 为TCP/IPV6
*/
destAddr.sin_family = AF_INET;
/*服务器监听的端口,就说如果有客户端发送数据只有发送到这个端口,服务器才能收到*/
destAddr.sin_port = htons(PORT);
addr_family = AF_INET;//通信协议使用TCP/IPV4
/*
* ip_protocol 指定此socket接收到协议包
* 注意:次处没有使用IPPROTO_TCP ,查看代码发现底层使用的是一套逻辑
*/
ip_protocol = IPPROTO_IP;
inet_ntoa_r(destAddr.sin_addr, addr_str, sizeof(addr_str) - 1);
#else // IPV6
struct sockaddr_in6 destAddr;
bzero(&destAddr.sin6_addr.un, sizeof(destAddr.sin6_addr.un));
destAddr.sin6_family = AF_INET6;
destAddr.sin6_port = htons(PORT);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
inet6_ntoa_r(destAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
#endif
/*
* 创建 socket,第二个参数为SOCK_STREAM,
* SOCK_STREAM的含义是提供面向连接的稳定数据传输,即TCP协议。
* 还可以将这个参数配置成SOCK_DGRAM 或者SOCK_RAW ,
* SOCK_DGRAM SOCK_DGRAM 是无保障的面向消息的socket,
* 主要用于在网络上发广播信息就是UDP
*
*/
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (listen_sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
int err = bind(listen_sock, (struct sockaddr *)&destAddr, sizeof(destAddr));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket binded");
/**
* 开始监听这个socket(套接字)连接的端口,
* 注意第二个参数为允许连接这个服务器的最多连接数
* 此处只允许一个客户端连接
*/
err = listen(listen_sock, 1);
if (err != 0) {
ESP_LOGE(TAG, "Error occured during listen: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket listening");
struct sockaddr_in6 sourceAddr; // Large enough for both IPv4 or IPv6
uint addrLen = sizeof(sourceAddr);
/**
* 等待有客户端连接这个服务器,accept()是一个阻塞函数,
* 在listen监听队列上没有链接时则死等在这里,当有连接时则往下执行
*/
int sock = accept(listen_sock, (struct sockaddr *)&sourceAddr, &addrLen);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket accepted");
/*连接成功,可以循环读写socket,完成网络通信*/
while (1) {
/**
* 接收socket上的数据
*/
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
// Error occured during receiving
if (len < 0) {
ESP_LOGE(TAG, "recv failed: errno %d", errno);
break;
}
// Connection closed
else if (len == 0) {
ESP_LOGI(TAG, "Connection closed");
break;
}
// Data received
else {
/**
* 接收len长度大于0 表示成功接收到了数据
*/
// Get the sender's ip address as string
if (sourceAddr.sin6_family == PF_INET) {//解析地址
inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
} else if (sourceAddr.sin6_family == PF_INET6) {
inet6_ntoa_r(sourceAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
}
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
ESP_LOGI(TAG, "%s", rx_buffer);
/**
* 向客户端发送数据
*/
int err = send(sock, rx_buffer, len, 0);
if (err < 0) {
ESP_LOGE(TAG, "Error occured during sending: errno %d", errno);
break;
}
}
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);/关闭套接字
}
例程演示
1.进入例程 执行make menuconfig 命令配置esp32对应的串口
2 配置wifi 账户密码和服务器端口。
3 执行make -j8 flash monitor,socket 创建成功 等待连接
4. 打开手机,在应用市场下载socket 测试app。打开app,创建一个tcp client。 输入esp32 对应的ip地址和端口,点击连接按钮。
注意:esp32 和手机必须在用一个网络中
5.连接成功,esp32终端会输出 socket accepted
6.收发数据
手机端app 输入hi esp32 点击发送,esp32终端则会收到这串字符并返回给手机app:
写着最后 :
socket网络套接字是网络编程中最简单也是使用最频繁的技术。如果有志成为物联网开发的工程师的话socket编程是必须掌握的基础知识。