本篇来使用WIFI进行TCP无线通信测试,测试OK3568-C开发板与ESP8266进行无线通信,另外,服务端还设计了多客户端的处理功能,除了与ESP8266进行通信外,还可以与其它的TCP客户端进行通信,比如Ubuntu虚拟机创建的客户端。
1 TCP通信
1.1 TCP通信基础原理
TCP是一个面向连接的传输层协议,在数据发送之前(即进程通信之前),必须先建立连接。通信完毕后,必须关闭连接。基于TCP传输协议的服务器与客户机间的通信工作流程如下图:
TCP通信的大致流程如下:
- 服务器先用 socket() 函数来建立一个套接字,用这个套接字完成通信的监听及数据的收发。
- 服务器用 bind() 函数来绑定一个端口号和IP地址,使套接字与指定的端口号和IP地址相关联。
- 服务器调用 listen() 函数,使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。
- 客户机用 socket() 函数建立一个套接字,设定远程IP和端口。
- 客户机调用 connect() 函数连接远程计算机指定的端口。
- 服务器调用 accept() 函数来接受远程计算机的连接请求,建立起与客户机之间的通信连接。
- 建立连接以后,客户机用 write() 函数(或 close() 函数)向socket中写入数据,也可以用 read() 函数(或 recv() 函数)读取服务器发来的数据。
- 服务器用 read() 函数(或 recv() 函数)读取客户机发来的数据,也可以用 write() 函数(或 send() 函数)来发送数据。
- 完成通信以后,使用 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_data_t data;
};
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虚拟机创建的客户端进行通信。