前言
在选购好NB开发板之后,我们就可以使用AT指令进行socket通信了,主要问题在于我们之前学习socket编程只需要一台电脑就可以进行学习,服务器和客户端都是在本机上运行,或者是两台处于同一局域网下的主机之间进行互相通信,而NB-IOT芯片信号是通过空中接口(Uu)发送到基站(eNB)信号塔(此段仅为个人理解),个人主机是在局域网环境下运行的,没有公网IP(可直接连接Internet),基站信号塔是找不到我们个人主机的,NB开发板就无法跟我们的主机进行通信,所以我们还需要准备一台具有公网IP的服务器,读者可以选择购买阿里或腾讯的服务器。
进行开发首先需要在个人主机上编写好程序,然后拷贝到咱们的服务器上面,这个过程可以参考我前面一篇文章 文章链接,配置好之后咱们就可以进行实验了
TCP/IP
BC26还专门出了一组TCP通信的AT指令手册:《Quectel_BC26&BC20_TCP(IP)_AT_V1.1_Preliminary》,之前就有TCP与UDP的socket通信的命令手册,这里我也不了解,可能是为了兼容,当然了TCP/IP也可以进行UDP通信,本章不讲解UDP。
命令介绍
TCP是一种面向连接、可靠的通信协议,TCP发送的信息对方必须接收到,如果出现差错或者丢失数据也可以进行重传,前提是对方必须知道,而UDP是一种面向无连接的协议,只管发送信息,至于对方是否接收到,就不用咱们关心了,接下来将介绍TCP/IP使用socket通信所用到的命令。
AT+QIOPEN=<contextID>,<connectID>,<service_type>,<IP_address>/<domain_name>,<remote_port><local_port>,<access_mode>,<<protocol_type>
打开一个Socket服务
<contextID>
:整形,上下文ID,范围:1-3,目前仅支持设置为1<connectID>
:整形,Socket服务索引,范围:0-4<service_type>
:字符串类型,Socket协议类型,"TCP"或"UDP"<IP_address>
:字符串类型,远程服务器的IP地址domain_name>
:字符串类型,远程服务器的域名地址<remote_port>
:远程服务器的端口<local_port>
:指定本地端口号,一般为0,系统会自动分配本地端口号<access_mode>
:整形,Socket的数据访问模式,0为缓存访问模式 1为直接访问模式<protocol_type>
:整形,IP协议类型 0为IPv4 1为IPv6
AT+QICLOSE=<connectID>
关闭一个Socket服务
<connectID>
:整形,Socket服务索引,范围0-4
AT+QISTATE=<query_type>,<contextID>
查询Socket连接状态
<query_type>
:整形,表示查询类型,根据通过<contextID>
0 或<contextID>
1 来查询连接状态<contextID>
:整形,上下文ID,范围:1-3,目前仅支持设置为1
AT+QISEND=<connectID>,<send_length>,<data>
向服务器发送十六进制/文本字符串数据
<connectID>
:整形,Socket服务索引,范围:0-4<send_length>
:待发数据长度,单位:字节<data>
:待发送数据
AT+QISEND=<connectID>
发送不定长数据
向服务器发送数据,待响应“>” 后,输入要发送的数据,按"Ctrl + z"发送数据,按“Esc”取消发送
AT+QISEND=<connectID>,<send_length>
发送定长数据
向服务器发送数据,待响应“>” 后,输入长度等于<send_length>
的待发数据
AT+QISENDEX=<connectID>,<send_length>,<hex_string>
发送十六进制字符串数据
<connectID>
:整形,Socket服务索引,范围:0-4<send_length>
:待发数据长度,最大字节512<hex_string>
:待发送的十六进制字符串数据
AT+QIRD=<connectID>,<read_length>
将接收到的数据缓存并读取
<connectID>
:整形,Socket服务索引,范围:0-4<read_length>
:读取最大长度数据,最大值为512字节
其余命令暂时用不上,这里就不在一一列举,TCP/IP命令手册获取 命令手册连接 提取码:eu6h
数据访问模式
BC26模块支持两种数据访问模式
缓存访问模式直接访问模式
当通过AT+QIOPEN
打开一个 Socket 服务时,可以通过参数<access_mode>
来指定数据访问模式。
当 Socket 服务成功打开,可以通过AT+QISWTMD
改变数据访问模式。
1:在缓存访问模式下,可通过命令AT+QISEND/AT+QISENDEX
发送数据。当接收到数据时,模块会
缓存所接收的数据,并有 URC 上报,格式为:+QIURC: "recv",<connectID>[,<current_recv_length>]
,模块
可以通过命令AT+QIRD
来读取缓存数据
2:直接访问模式下,可通过命令AT+QISEND/AT+QISENDEX
发送数据。模块新接收的数据会通过
上报 URC 格式为:+QIURC: "recv",<connectID>, <currect_recv_length><CR><LF><data>
直接输出
服务器/客户端程序
程序跟之前无变化,可以接收多个连接并回射数据
服务端
#include <stdio.h> //printf#include <sys/socket.h> //socket#include <netinet/in.h> //sockaddr_in#include <arpa/inet.h> //htons inet_addr#include <unistd.h> //close#include <stdlib.h> //exit#include <string.h> #include <sys/wait.h> //wait waitpid#include <signal.h> //signal#define MAXSIZE 255void error_hanglde(char *message);void process_hanglde(int sig);int main(int argc,const char *argv[]){int sockfd,cli_sock;ssize_t n;socklen_t clilen;char message[MAXSIZE];pid_t pid;struct sockaddr_in ser_addr,cli_addr;struct sigaction act;if(argc != 3){fprintf(stderr,"Usage %s ip potr\n",argv[0]);}sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1)error_hanglde("sockfd() error!");bzero(&ser_addr,sizeof(ser_addr));ser_addr.sin_family = AF_INET;ser_addr.sin_addr.s_addr = inet_addr(argv[1]); //设置IP地址,如果是阿里云,要设置成内网IP地址,而不是公网IP地址ser_addr.sin_port = htons(atoi(argv[2]));//端口号if(bind(sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)) == -1)error_hanglde("bind() error!");if(listen(sockfd,10) == -1)error_hanglde("listen() error!");act.sa_handler = process_hanglde;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGCHLD,&act,0);//进行通信for(;;){clilen = sizeof(cli_addr);cli_sock = accept(sockfd,(struct sockaddr *)&cli_addr,&clilen);if(cli_sock == -1)continue;elseprintf("[%s - %d] Connect Client:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),cli_sock);//创建子进程pid = fork();if(pid == -1){close(cli_sock);continue;}if(pid == 0) //子进程执行区域{close(sockfd); //关闭监听套接字while((n = read(cli_sock,message,MAXSIZE)) != 0) //数据收发{write(cli_sock,message,n);message[n] = 0;}printf("[%s - %d] client disconnte:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),cli_sock);close(cli_sock);return 0;}}close(sockfd);close(cli_sock);return 0;}void process_hanglde(int sig){pid_t pid;int status;pid = waitpid(-1,&status,WNOHANG); if(WIFEXITED(status)){printf("process recv id = %d\n",pid);}}void error_hanglde(char *message){fputs(message,stderr);fputs("\n",stderr);exit(0);}
客户端
#include <stdio.h> //printf#include <sys/socket.h> //socket#include <netinet/in.h> //sockaddr_in#include <arpa/inet.h> //htons inet_addr#include <unistd.h> //close#include <stdlib.h> //exit#include <string.h> #define MAXSIZE 255void error_hag(char *message);int main(int argc,const char *argv[]){int sockfd;ssize_t n;char message[MAXSIZE],ReadLine[MAXSIZE];if(argc != 3){fprintf(stderr,"Usage %s ip potr\n",argv[0]);}sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1)error_hag("sockfd() error");struct sockaddr_in ser_addr;bzero(&ser_addr,sizeof(ser_addr));ser_addr.sin_family = AF_INET;ser_addr.sin_addr.s_addr = inet_addr(argv[1]); //设置为公网IP地址ser_addr.sin_port = htons(atoi(argv[2]));//对应服务器端口号if(connect(sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)) == -1)error_hag("connect() error");for(;;){fputs("Input info(Q ~ Quit):",stdout);fgets(message,MAXSIZE,stdin);if(strcmp(message,"Q\n") == 0 || strcmp(message,"Quit\n") == 0)break;write(sockfd,message,strlen(message));n = read(sockfd,ReadLine,MAXSIZE);ReadLine[n] = 0;fputs(ReadLine,stdout); }close(sockfd);}void error_hag(char *message){fputs(message,stderr);fputs("\n",stderr);exit(0);}
启动程序后就可以去做实验了,这里我用的是我们老师自己做的工具:AT指令助手,个人用起来还是蛮舒服的,功能也很强大,很方便,AT助手工具获取链接 提取码:k05h
缓存访问模式
咱们开始先介绍BC26数据类型模式:缓存访问模式
接下来发送数据,用AT+QISEND
和AT+QISENDEX
这两个命令
发送定长数据
//打开一个 Socket 服务>>>>>>>>>> AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0 AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0OK+QIOPEN: 0,0//发送长度为11字节的文本字符串>>>>>>>>>> AT+QISEND=0,11,Hello WorldAT+QISEND=0,11,Hello WorldOKSEND OK+QIURC: "recv",0 //上报URC,0表示接收到数据并缓存//发送长度为5字节的十六进制字符串 “01234”>>>>>>>>>> AT+QISENDEX=0,5,3031323334AT+QISENDEX=0,5,3031323334OKSEND OK//关闭 Socket 连接>>>>>>>>>> AT+QICLOSE=0AT+QICLOSE=0OKCLOSE OK
第一次发送的是字符串数据,服务器返回数据并上报URC,第二次发送的是进制形式,服务器接收到数据但没有上传到URC,原因是第一次接收的缓存数据未清空。
发送不定长数据
发送不定长数据需要用到AT+QISEND=0
,此时服务器会响应一个“ > ”,等待用户输入数据。
>>>>>>>>>> AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0OK+QIOPEN: 0,0//发送不定长数据>>>>>>>>>> AT+QISEND=0AT+QISEND=0//待发数据>>>>>>>>>>> xrl.yydsOKSEND OK+QIURC: "recv",0 // 0 上报URC 接收到数据//关闭 Scoket 连接>>>>>>>>>> AT+QICLOSE=0AT+QICLOSE=0OKCLOSE OK
数据的接收
数据的接收需要使用AT+QIRD
这个命令
//打开一个 Socket 服务>>>>>>>>>> AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0OK+QIOPEN: 0,0//返回0,0表示无误 连接成功//发送长度为11字节的字符串>>>>>>>>>> AT+QISEND=0,11,Hello WorldAT+QISEND=0,11,Hello WorldOKSEND OK+QIURC: "recv",0 // 0 上报URC,接收到数据//读取接收到的数据,最大长度为512字节,防止过长溢出>>>>>>>>>> AT+QIRD=0,512AT+QIRD=0,512+QIRD: 11//收到长度为11字节Hello World //收到长度为11字节的字符串OK//再次接收>>>>>>>>>> AT+QIRD=0,512AT+QIRD=0,512+QIRD: 0 // 0 表示接收区为空OK//关闭 Scoket 连接>>>>>>>>>> AT+QICLOSE=0AT+QICLOSE=0OKCLOSE OK
其次也可以指定接收的长度
//打开一个 Socket 服务>>>>>>>>>> AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0AT+QIOPEN=1,0,"TCP","120.79.83.71",9999,0,0OK+QIOPEN: 0,0 //返回0,0表示无误 连接成功//发送长度为11字节的字符串>>>>>>>>>> AT+QISEND=0,11,Hello WorldAT+QISEND=0,11,Hello WorldOKSEND OK+QIURC: "recv",0 // 0 上报URC,接收到数据>>>>>>>>>> AT+QIRD=0,5 AT+QIRD=0,5 //指定接收数据的长度为5字节+QIRD: 5 //收到长度为5字节数据HelloOK//关闭 Scoket 连接>>>>>>>>>> AT+QICLOSE=0AT+QICLOSE=0OKCLOSE OK
直接访问模式
使用直接访问模式会在发送数据后上传给URC直接显示接收到的数据
可以看出,如果接收到的数据量较小的话,直接访问模式还是很方便的。
结尾
文章如有误望指正,也希望本文章能帮助读者,临近毕业,对口相关岗位还是很难找,不知道后面会不会坚持下来从事IT行业