飞凌嵌入式
直播中

fsdzdzy

9年用户 260经验值
擅长:嵌入式技术 控制/MCU opencv
私信 关注
[技术]

【飞凌RK3568开发板试用体验】13-与ESP8266进行TCP通信测试

本篇来使用WIFI进行TCP无线通信测试,测试OK3568-C开发板与ESP8266进行无线通信,另外,服务端还设计了多客户端的处理功能,除了与ESP8266进行通信外,还可以与其它的TCP客户端进行通信,比如Ubuntu虚拟机创建的客户端。

1 TCP通信

1.1 TCP通信基础原理

TCP是一个面向连接的传输层协议,在数据发送之前(即进程通信之前),必须先建立连接。通信完毕后,必须关闭连接。基于TCP传输协议的服务器与客户机间的通信工作流程如下图:

TCP通信的大致流程如下:

  1. 服务器先用 socket() 函数来建立一个套接字,用这个套接字完成通信的监听及数据的收发。
  2. 服务器用 bind() 函数来绑定一个端口号和IP地址,使套接字与指定的端口号和IP地址相关联。
  3. 服务器调用 listen() 函数,使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。
  4. 客户机用 socket() 函数建立一个套接字,设定远程IP和端口。
  5. 客户机调用 connect() 函数连接远程计算机指定的端口。
  6. 服务器调用 accept() 函数来接受远程计算机的连接请求,建立起与客户机之间的通信连接。
  7. 建立连接以后,客户机用 write() 函数(或 close() 函数)向socket中写入数据,也可以用 read() 函数(或 recv() 函数)读取服务器发来的数据。
  8. 服务器用 read() 函数(或 recv() 函数)读取客户机发来的数据,也可以用 write() 函数(或 send() 函数)来发送数据。
  9. 完成通信以后,使用 close() 函数关闭socket连接。

1.2 端口号介绍

在TCP通信中,需要用到端口号,端口大致有两种意思:一是物理意义上的端口,比如ADSL Modem、集线器、交换机、路由器等用于连接其它网络设备的接口,如RJ-45端口、SC端口等。二是逻辑意义上的端口,一般指TCP/IP协议中的端口,端口范围从0~65535,比如浏览器网页服务(HTTP协议)的80端口,用于FTP服务的21端口等。端口号只有本地意义,即端口号是为了标识本地计算机的各个进程。

端口号分为两类,一类是由因特网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的”周知的端口“,其数值一般为0~1024,如:

在实际使用时,自己使用的端口号,不要与”周知的端口“相同。

2 epoll

epoll的全称为eventpoll,是linux内核实现IO多路复用的一个实现。epoll是select和poll的升级版,相较于这两个前辈,epoll改进了工作方式,使之更加高效。

下面来介绍如何使用epoll来实现多路复用功能。

2.1 epoll创建

int epoll_create(int size); //监听个数

2.2 epoll事件设置

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

第一个参数epfd是epoll_create()的返回值, 第二个参数op表示动作,用三个宏来表示:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd, 第四个参数是告诉内核需要监听什么事, struct epoll_event结构如下:

struct epoll_event {  
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

events可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

2.3 epoll监听

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

等待事件的产生,类似于select()调用。 参数events用来从内核得到事件的集合, maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size, 参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。 该函数返回需要处理的事件数目,如返回0表示已超时。

epoll相关的内部数据结构示意如下:

2.4 epoll工作模式

  • 水平模式:简称为 LT (level triggered)模式,是缺省的工作方式,并且同时支持block和no-block socket。
    在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行 IO 操作了。如果我们不作任何操作,内核还是会继续通知使用者。
  • 边沿模式:简称为 ET (edge-triggered)模式,是高速工作方式,只支持no-block socket。
    在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。如果我们对这个文件描述符做 IO 操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

3 服务端程序

服务端程序运行在OK3568-C板子中,并具有epoll的多路处理功能,可以处理多个客户端的请求

void tcp_server_thread()
{
//创建服务器端套接字文件
int listenfd=socket(AF_INET, SOCK_STREAM, 0);
    
//初始化服务器端口地址
    struct sockaddr_in tcpServerAddr;
bzero(&tcpServerAddr, sizeof(tcpServerAddr));
tcpServerAddr.sin_family=AF_INET;
tcpServerAddr.sin_addr.s_addr= htonl(INADDR_ANY);
tcpServerAddr.sin_port=htons(SERV_PORT);
    
//将套接字文件与服务器端口地址绑定
bind(listenfd, (struct sockaddr *)&tcpServerAddr, sizeof (tcpServerAddr)) ;
    
//监听,并设置最大连接数为20
listen(listenfd, 20);
printf("[%s] Accepting connections... \n", __func__);
    
    //通过epoll来监控多个客户端的请求
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    int num;
    char buf[MAXSIZE];
    memset(buf,0,MAXSIZE);
    epollfd = epoll_create(FDSIZE);
    printf("[%s] create epollfd:%d\n", __func__, epollfd);
​
    //添加监听描述符事件
    epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, listenfd, EPOLLIN);
    while(1)
    {
        //获取已经准备好的描述符事件
        printf("[%s] epollfd:%d epoll_wait...\n", __func__, epollfd);
        num = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        for (int i = 0;i < num;i++)
        {
            int fd = events[i].data.fd;
            //listenfd说明有新的客户端请求连接
            if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            {
                //accept客户端的请求
                struct sockaddr_in cliaddr;
                socklen_t  cliaddrlen = sizeof(cliaddr);
                int clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
                if (clifd == -1)
                {
                    perror("accpet error:");
                }
                else
                {
                    printf("[%s] accept a new client(fd:%d): %s:%d\n",
                           __func__, clifd, inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
                    //将客户端fd添加到epoll进行监听
                    epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, clifd, EPOLLIN);
                }
            }
            //收到已连接的客户端fd的消息
            else if (events[i].events & EPOLLIN)
            {
                memset(buf,0,MAXSIZE);
                //读取客户端的消息
                int nread = read(fd,buf,MAXSIZE);
                if (nread == -1)
                {
                    perror("read error:");
                    close(fd);
                    epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
                }
                else if (nread == 0)
                {
                    printf("[%s] client(fd:%d) close.\n", __func__, fd);
                    close(fd);
                    epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
                }
                else
                {
                    //将客户端的消息打印处理, 并表明是哪里客户端fd发来的消息
                    printf("[%s] read message from fd:%d ---> %s\n", __func__, fd, buf);
                }
            }
        }
    }
​
    close(epollfd);
}

4 客户端程序

客户端程序通过TCP与服务端进行相连,只要具有连网功能的硬件,理论上都可以作为客户端。这里测试ESP8266开发板和Ubuntu虚拟机作为两个客户端,测试与OK3568的通信功能

4.1 ESP8266客户端程序

#include <ESP8266WiFi.h>
​
const char* ssid = "xxx"; //<----修改为自己的
const char* password = "xxx"; //<----修改为自己的
​
const uint16_t port = 6666;
const char *host = "192.168.5.182";
WiFiClient esp8266Client;
​
void setup()
{
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  delay(500);
  Serial.print("连接到:");
  Serial.println(ssid);
  uint8_t i = 0;
  while (WiFi.status() != WL_CONNECTED && i++ < 20)
  {
    delay(500);
  }
  if (i == 21)
  {
    Serial.print("没能连接到:");
    Serial.println(ssid);
    return ;
  }
​
  Serial.print("准备好了!使用的网络IP是: ");
  Serial.println(WiFi.localIP());
​
  Serial.print("连接到 ");
  Serial.println(host);
  if (!esp8266Client.connect(host, port))
  {
    Serial.println("TCP server端连接失败");
    Serial.println("请等待5秒后重新连接...");
    delay(5000);
    return;
  }
  Serial.println("TCP server端连接成功");
}
​
void loop()
{
  esp8266Client.print("This is ESP8266");
  delay(2000);
}

4.2 Ubuntu客户端程序

void tcp_client_thread(std::string serverIP)
{
    printf("[%s] in, prepare connect serverIP:%s\n", __func__, serverIP.c_str());
    
//创建客户端套接字文件
int tcpClientSocket= socket(AF_INET, SOCK_STREAM, 0);
 printf("[%s] create tcpClientSocket:%d\n", __func__, tcpClientSocket);
    
//初始化服务器端口地址
    struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr)) ;
servaddr.sin_family= AF_INET;
inet_pton(AF_INET, serverIP.c_str(), &servaddr.sin_addr);
servaddr.sin_port= htons(SERV_PORT);
    
//请求连接
connect(tcpClientSocket, (struct sockaddr*)&servaddr, sizeof (servaddr));
printf("[%s] connect\n", __func__);
    
    //要向服务器发送的信息
    char buf [MAXLINE];
std::string msg = "this is ubuntu20";
    while(1)
    {
        //发送数据
        send(tcpClientSocket, msg.c_str(), msg.length(),0);
        printf("[%s] send to server: %s\n", __func__, msg.c_str());
        
        //接收服务器返回的数据
        int n= recv(tcpClientSocket, buf, MAXLINE, MSG_DONTWAIT); //非阻塞读取
        if(n>0)
        {
            printf("[%s] Response from server: %s\n", __func__, buf);
        }
        
        sleep(2);
    }
//关闭连接
close(tcpClientSocket) ;
printf("[%s] end\n", __func__);
}

5 测试

5.1 交叉编译服务端程序

本篇的TCP服务端程序还没有用到Qt功能,仅用了C++的相关代码,因此只需要使用g++编译器编译即可,编译前先临时设置一下环境变量:

export PATH=/home/xxpcb/myTest/OK3568/gcc_aarch64/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH

然后就可以使用对应的g++编译器进行编译了,最后使用file指令确认下文件的类型:

5.2 编译运行ESP8266程序

ESP8266程序使用的是Arduino IDE进行编程,Arduino IDE还自带了串口监视器,通过串口监视器可以查看程序的运行结果:

5.3 多客户端连接测试

  • 首先在OK3568-C板子上启动TCP服务端程序server
  • 然后启动ESP8266TCP客户端和UbuntuTCP客户端,启动顺序无要求
  • 观察服务端的打印信息,可以看到服务端收到了ESP8266TCP客户端和UbuntuTCP客户端发来的信息

6 总结

本篇测试OK3568-C开发板与ESP8266进行通信,通过WIFI实现TCP无线连接。OK3568-C上运行的TCP服务端还设计了多客户端的处理功能,在与ESP8266进行通信的同时,还可以与Ubuntu虚拟机创建的客户端进行通信。

更多回帖

发帖
×
20
完善资料,
赚取积分