无线传感网

使用AT指令进行Socket通信

作者:陈广
日期:2021-4-29


之前在学BC26的时候写过Socket通信,只是现在芯片换了,指令也变了,需要重写。之前的文章需要有自己的服务器方能完成操作,现在这篇文章针对班上的学生,上课的时候自己架一个服务器所有人可以同时使用,所以服务器端也需要重写。另外,之前用的是.NET Core 2.0,现在也更新为.NET Core 5.0了,所以服务器的架设也需要重新来一遍。本次实验基于腾讯云服务器,操作系统为CentOS 7.5 64位版本。

安装.NET Core 5.0

在安装 .NET 之前,运行以下命令,将 Microsoft 包签名密钥添加到受信任密钥列表,并添加 Microsoft 包存储库。使用以下命令:

sudo rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm

接下来安装SDK

sudo yum install dotnet-sdk-5.0

安装web服务器Jexus

使用以下命令安装Jexus:

curl https://jexus.org/release/x64/install.sh|sh

准备工作相当简单,接下来可以写程序了。

编写服务器程序

接下来,编写服务器程序,非常简单,在接到客户端发送来的东西原样返回。新建.NET Core 5.0控制台项目,名称为【TCPSocket】。记住这个名字,如果用其它名称,布署的时候则需要填上相应名称。代码如下:

static void Main(string[] args)
{
    //设置服务器 IP,如果是腾讯云,必须使用内网地址,而不是公网 IP。
    IPAddress ip = IPAddress.Parse("172.16.0.7");
    IPEndPoint point = new IPEndPoint(ip, 5000); //端口指定为 5000
    Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    try
    {
        s.Bind(point);
        s.Listen(60);
        Console.WriteLine("服务器开始侦听...");
        while (true)
        {
            Socket subSocket = s.Accept();
            Console.WriteLine("获取一个来自{0}的连接", subSocket.RemoteEndPoint.ToString());
            //创建专门的线程去监听客户端发送信息请求
            Task.Factory.StartNew(() => ReceiveMessage(subSocket), TaskCreationOptions.LongRunning);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
    finally
    {
        s.Close();
    }
}

//监听客户端连接的线程方法
static void ReceiveMessage(Socket subSocket)
{
    byte[] buff = new byte[1024]; //创建一个接收缓冲区
    try
    {
        while (true)
        {
            int count = subSocket.Receive(buff, buff.Length, SocketFlags.None);
            //下面这个判断是非常必要的,否则有可能导致不停地接收到长度为 0 的数据,导致 CPU 占用率100%
            if (count == 0)
            {
                subSocket.Close();
                return;
            }
            //将接收到的数据转原封不动返回
            subSocket.Send(buff, count, SocketFlags.None);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
    finally
    {
        subSocket.Close();//客户端关闭时会引发异常,此时关闭此连接
        Console.WriteLine("客户端已退出连接。");
    }
}

程序写完,运行没有问题后,点Visual Studio菜单【生成】➤【发布TCPSocket】。在【..\bin\Release\net5.0\publish】文件夹下可以的看到发布的文件,需要将所有文件拷贝到服务器上。

将应用程序布署到服务器

将文件传到服务器上比较麻烦,建议使用现成的软件,这里推荐使用“FileZilla Client”,相当好用。打开FileZilla,在【/var】文件夹下新建一个【www】的文件夹,将刚才发布的文件拷贝到这个文件夹下,如下图所示:

接下来在FileZilla中进入到【/usr/jexus/siteconf】文件夹,将里面的default文件拷贝到本地,记事本打开,按下图进行更改:

更改完成后将文件更名为aspnetcore。

接下来回来FileZilla,删除【/usr/jexus/siteconf】文件夹下的default文件,将aspnetcore上传替代它。

最后使用如下命令重启jexus:

sh /usr/jexus/jws restart

如果没有启动,则使用启动命令

sh /usr/jexus/jws start

这样,应用程序就布署完毕了,可以写个控制台程序测试一下:

static void Main(string[] args)
{
    IPAddress ip = IPAddress.Parse("42.194.141.9"); //注意IP地址使用实际的IP地址
    try
    {
        Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        s.Connect(ip, 5000); //注意端口号是刚才写的5000
        Console.WriteLine("开始连接服务器 {0} ...", ip.ToString());
        if (s.Connected)
        {
            Console.WriteLine("连接成功!开始发送数据...");
        }
        string sendStr = "HI!这是一个 socket 测试!";
        byte[] sendBuff = Encoding.Unicode.GetBytes(sendStr);//创建发送缓冲
        s.Send(sendBuff, sendBuff.Length, SocketFlags.None);//发送

        //接收服务器端传来的字符串
        byte[] buff = new byte[1024];
        int count = s.Receive(buff, buff.Length, SocketFlags.None);
        string recvStr = Encoding.Unicode.GetString(buff, 0, count);
        Console.WriteLine("从服务器收到信息:{0}", recvStr);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

注意,IP地址和端口要按实际情况来填写。如果程序运行结果是下面样子,说明服务器没问题了:

开始连接服务器 42.194.141.9 ...
连接成功!开始发送数据...
从服务器收到信息:HI!这是一个 socket 测试!

AM22E的TCP命令

万事俱备,终于可以发数据了。首先介绍几个TCP相关的AT指令。这部份内容翻译自AT命令手册,在学习的时候,有些命令如果不太清楚使用方法,最好的办法就是阅读命令手册,足够详细,只是全英文对部分同学来说有些困难。

AT+IPSTART

此命令创建一个TCP或UDP socket并连接至远程服务器。

语法:
设置命令:

AT+IPSTART=<sockid>,<type>,<addr>,<port>[,<cid>[,<domian>[,<protocol>]]]

参数:

  • <sockid>:整数,socket信道号,范围0-4。也就是说,最多可以同时运行5个socket。
  • <type>:字符串
    • "TCP":TCP socket
    • "UDP":UDP socket
    • "RAW":RAW socket
  • <addr>:字符串,远程地址。此命令不会进行合法性检查,必须确保输入的IP地址合法。
  • <port>:整数,远程端口。
  • <cid>:整数PDP上下文ID,由AT+EGACT命令响应。
  • <domain>:整数,默认值为 2。其中:
    • 2:表示IPv4
    • 10:表示IPv6
  • <protocol>:发送的数据包数量,默认值为0,现今仅支持0
    • 0:表示IP
    • 1:表示ICMP
    • 6:表示TCP
    • 17:表示UDP

AT+IPSEND

此命令用于向网络发送数据,如果响应“OK”,仅表示AT命令格式正确,并且数据已经放入socket,等待发送。

语法:

TCP :
AT+IPSEND=<socket_id>,[<data_len>],<data>[,<pri_flag>]

UDP :
AT+IPSEND=<socket_id>,[<data_len>],<data>[,<addr>,<port>[,<pri_flag>]]

成功则返回:
+IPSEND: <socket_id>,<sent_len>
OK

失败则返回:
ERROR

参数:

  • <socket_id>:整数,socket的ID,可由AT+ESOC命令响应。
  • <data_len>:整数,数据的长度。取值范围0至720,默认值为0。
  • <data>:字符串,原始数据。当<data_len>大于0是时,<data>为十六进制格式串,如果<data_len>设置为0或省略,<data>为普通字符串。
    • <data_len>=0:普通字符串,数据长度范围为1-1440字节。
    • <data_len>省略:普通字符串,数据长度范围为1-1440字节。
    • <data_len>>0:十六进制串,取值范围1-720字节。
  • <addr>:字符串,远程地址,仅在UDP socket中有效。
  • <port>:整数,远程端口,仅在UDP socket中有效。
  • <pri_flag>:整数,优先级标志。
    • 0:IPTOS 优化可靠性,默认选项
    • 1:IPTOS 最小化延迟时间
    • 2:IPTOS 优化吞吐量
    • 3:IPTOS 最低成本
  • <sent_len>:整数,实际发送出去的数据长度

示例:

at+ipsend=0,0,"this is normal string"   //发送普通字符串
+IPSEND: 0,21
OK

at+ipsend=0,,"this is another normal string"   //发送普通字符串
+IPSEND: 0,29
OK

at+ipsend=0,2,"3132"    //发送十六进制串
+IPSEND: 0,2
OK

//对于 TCP socket:
at+ipsend=0,0,"this is normal string",1   //以低延迟方式发送普通字符串
+IPSEND: 0,21
OK

//For UDP socket:
at+ipsend=0,0,"this is normal string",,,1   //以低延迟方式发送普通字符串
+IPSEND: 0,21
OK

at+ipsend=0,0,"this is normal string","183.230.40.150",36000,1  //以低延迟方式向指定IP地址发送普通字符串
+IPSEND: 0,21
OK

注意:

  • <addr>,<port>省略(其中一项或两项参数缺省),则默认使用 AT+IPSTART 指定的地址和端口;若配置<addr>,<port>,该条指令将往所配置的地址和端口发送数据,该地址仅对此命令生效一次;
  • AT+IPSEND 中 TCP 与 UDP 指令有所区别,UDP 模式下第四、第五个参数为,,仅在UDP 模式下可以配置;TCP 模式第四个参数为整形的<pri_flag>,输入其他类型数据返回 ERROR;
  • S03 以前的版本 AT+IPSTART 采用域名的方式创建 Socket,由于解析 DNS,会在 AT+IPSTART 阻塞住,返回 OK 以后,Socket 创建成功,调用 AT+IPSEND 发送 UDP 数据,TCP 需等到CONNECT OK 以后,调用 AT+IPSEND 发送 TCP 数据,否则 AT+IPSEND 返回 ERROR;
  • S03 及以后的版本,域名的方式创建 Socket,不会发生阻塞,后台任务解析 DNS,UDP 需通过AT+IPSTATUS 确认 UDP 状态为 CONNECTED,才能调用 AT+IPSEND 发送 UDP 数据,TCP 需等到 CONNECT OK 以后,才能调用 AT+IPSEND 发送 TCP 数据,否则 AT+IPSEND 返回 ERROR。

AT+IPRCFG

此命令用于设置socket的接收配置。

语法:

+IPRCFG=<auto_receive>[,<mode>[,<hex>]]

如果成功:
OK

如果失败:
ERROR

参数:

  • <auto_receive>:整数,为1时,当数据到达,直接输出到AT端口;为0时,则需要使用 +IPRD 命令手动读取数据。
  • <mode>:整数,数据显示的格式:
    • 0:+IPRD: <socket_id>,<data_len>,<data>
    • 1:<data>
    • 2:+IPRD:<socket_id>,<remote_addr>,<remote_port>,<data_len>,<data>
  • <hex>:整数,为0时显示为字符串,为1时显示为十六进制串

示例:

at+ipstart=0,"TCP","47.93.217.230",2008
OK
CONNECT OK

at+iprcfg?   //当前设置
+IPRCFG: 1,0,0
OK
+IPRD: 0,15,hello, CMCC IOT  //自动接收了15字节数据

at+iprcfg=1,1,0    //仅输出数据
OK
hello, CMCC IOT    //接收15字节数据

at+iprcfg=1,2,0    //设置<mode>=2
OK
+IPRD: 0,"47.93.217.230",2008,15,hello, CMCC IOT  //显示IP和端口

at+iprcfg=1,2,1    //设置16进制格式显示
OK
+IPRD:
0,"47.93.217.230",2008,15,68656C6C6F2C20434D434320494F54

at+iprcfg=0,2,1    //手动接收数据
OK
+IPNMI: 0,15       //数据到达

at+iprd=0,512      //读取数据
+IPRD:
0,"47.93.217.230",2008,15,68656C6C6F2C20434D434320494F54
OK

AT+IPNMI

指示从网络接收到一些数据。

语法:

+IPNMI: <socket_id>,<data_len>

参数:

  • <socket_id>:整数,socket信道号
  • <data_len>:整数,传入数据的长度。如果使用 +IPRD 命令读取的长度小于<data_length>,几秒后如果没有再次读取,则将再次收到此命令指示剩余数据。

AT+IPRD

此命令用于手动读取socket数据

语法:

+IPRD=<socket_id>,<data_length>

如果成功:
OK

如果失败:
ERROR

参数:

  • <socket_id>:整数,socket信道标识
  • <data_length>:整数,需要读取的数据长度,如果实际接收的数据小于<data_length>,在 +IPRD 响应中会返回实际接收的数据长度。数据长度范围是1-1440字节。

AT+IPCLOSE

此命令用于断开并关闭socket。如果socket为TCP,它将开始发送TCP FIN数据包;如果socket为UDP,将不会发送数据包。

语法:

+IPCLOSE=<socket_id>

成功返回:
OK

失败返回:
ERROR

参数:

  • <socket_id>:整数,socket ID。

使用NB-IOT的TCP连接服务器

接下来可以深度连接服务器收发数据:

例一:以字符串形式收发数据

>>>>>>>>>>  ATE0  //关闭AT命令回响
ATE0
OK

>>>>>>>>>>  AT+IPSTART=0,"TCP","42.194.141.9",5000  //尝试连接远程服务器42.194.141.9:5000,socket编号为0
OK
CONNECT OK  //连接成功

>>>>>>>>>>  AT+IPSEND=0,0,"Hello World"  //发送字符串:Hello World
+IPSEND: 0,11  //提示已发送了11个字节
OK
+IPNMI: 0,11  //提示接收到11个字节(服务器端原样返回的数据)

>>>>>>>>>>  AT+IPRCFG=1,0,0  //设置接收方式为自动接收,
                             //显示格式包含:socket ID、数据长度、数据
                             //以字符串形式显示
OK
+IPRD: 0,11,Hello World  //自动接收了刚才收到的数据

>>>>>>>>>>  AT+IPSEND=0,0,"This is second string" //发送第二个字符串
+IPSEND: 0,21
OK

+IPRD: 0,21,This is second string  //自动接收到第二个字符串

>>>>>>>>>>  AT+IPCLOSE=0  //关闭socket
OK

上例以字符串的形式发送了两个字符串,并设置自动接收。

例二:以字节形式收发数据

>>>>>>>>>>  AT+IPSTART=0,"TCP","42.194.141.9",5000 //创建socket
OK
CONNECT OK  //表示连接成功

>>>>>>>>>>  AT+IPRCFG=1,1,0  //自动接收,并仅显示数据
OK

>>>>>>>>>>  AT+IPSEND=0,4,"41424344"  //发送4个字节的数据
+IPSEND: 0,4
OK

ABCD  //自动接收到4个字节数据,并显示为字符串

>>>>>>>>>>  AT+IPRCFG=1,1,1  //设置为自动接收,并显示为十六进制串
OK

>>>>>>>>>>  AT+IPSEND=0,4,"41424344"  //发送4个字节数据
+IPSEND: 0,4
OK

41424344  //自动接收到4字节数据,并显示为十六进制串

>>>>>>>>>>  AT+IPCLOSE=0  //关闭socket
OK

例三:手动接收数据

>>>>>>>>>>  AT+IPSTART=0,"TCP","42.194.141.9",5000 //连接服务器
OK
CONNECT OK  //连接成功

>>>>>>>>>>  AT+IPRCFG=0,0,1  //设置为手动接收,十六进制显示
OK

>>>>>>>>>>  AT+IPSEND=0,8,"4142434445464748"  //发送8个字节
+IPSEND: 0,8  //已经成功发送8个字节
OK

+IPNMI: 0,8  //

>>>>>>>>>>  AT+IPRD=0,3  //指示socket去接收3个字节的数据
+IPRD: 0,3,414243  //接收到3个字节的数据
OK

+IPNMI: 0,5  //

>>>>>>>>>>  AT+IPRD=0,5  //指示socket去接收5个字节的数据
+IPRD: 0,5,4445464748  //接收到5个字节的数据
OK

>>>>>>>>>>  AT+IPCLOSE=0  //关闭连接
OK

UDP通信

UDP 协议具有资源消耗小,处理速度快的优点,但它是不可靠的。接下来演示使用UDP协议进行通信,使用UDP和使用TCP的思维方式是不一样的。UDP没有连接,也就没有所谓的断开连接,但有意思的是,使用AT指令发送UDP信息时,依然和TCP一样,需要进行连接和断开连接操作(在C#中写UDP程序是没有这些的)。你不能说建立一个连接后,在这个连接的基础上你来我往。UDP 的一个Socket只会侦听某一端口的所有信息,而这个信息可能是不同客户端发送的,所以,每次接收信息都要创建一个新的IPEndPoint。

服务器代码

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Text;

namespace UDPSocket
{
    class Program
    {
        static void Main(string[] args)
        {
            //设置服务器 IP,如果是腾讯云,必须使用内网地址,而不是公网 IP。
            IPAddress ip = IPAddress.Parse("172.16.0.7");
            IPEndPoint point = new IPEndPoint(ip, 5000); //端口指定为 5000
            Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            udpSocket.Bind(point);
            Console.WriteLine("服务器开始侦听...");
            byte[] buff = new byte[1024]; //创建一个接收缓冲区
            try
            {
                while (true)
                {
                    EndPoint remote = new IPEndPoint(IPAddress.Any, 0);
                    int count = udpSocket.ReceiveFrom(buff, ref remote);
                    //将接收到的数据原样返回
                    udpSocket.SendTo(buff, 0, count, 0, remote);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                udpSocket.Close();
            }
        }
    }
}

注意,在真实服务器端程序中,不要试图用Console.ReadLine()方法让控制台应用程序不关闭,它不起作用,这个坑我记得之前遇到过,忘记了,导致又踩了半天坑才爬上来。

客户端测试代码

可以写个C#程序测试服务器是否可用

IPAddress ip = IPAddress.Parse("42.194.141.9"); //注意IP地址使用实际的IP地址
IPEndPoint point = new IPEndPoint(ip, 5000);
try
{
    Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    string sendStr = "HI!这是一个 socket 测试!";
    byte[] sendBuff = Encoding.Unicode.GetBytes(sendStr);//创建发送缓冲
    s.SendTo(sendBuff, sendBuff.Length, SocketFlags.None, point);

    //接收服务器端传来的字符串
    byte[] buff = new byte[1024]; //创建一个接收缓冲区
    try
    {
        while (true)
        {
            EndPoint remote = new IPEndPoint(IPAddress.Any, 0);
            int count = s.ReceiveFrom(buff, ref remote);
            //将接收到的数据转化为 ASCII 字符
            string recvStr = Encoding.Unicode.GetString(buff, 0, count);
            Console.WriteLine($"接收到来自{remote.ToString()}数据:{recvStr}");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
    finally
    {
        s.Close();
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}
Console.ReadLine();

如果运行结果如下所示,则说明服务器运行没有问题。

接收到来自42.194.141.9:5000数据:HI!这是一个 socket 测试!

注意,在重布置应用程序的时候,除了更换Visual Studio发布的内容外,还要更改aspnetcore文件中指定的dll名称,两者都更改完毕后,重启Jexus即可。

例四:UDP通信

>>>>>>>>>>  AT+IPRCFG=1,0,0  //设置接收方式为自动接收,字符串显示
OK

>>>>>>>>>>  AT+IPSTART=0,"UDP","42.194.141.9",5000  //创建UDP socket
OK

>>>>>>>>>>  AT+IPSTATUS=0  //查看Socket状态
+IPSTATUS: 0,"UDP","42.194.141.9",5000,"CONNECTED"  //表明连接成功
OK

>>>>>>>>>>  AT+IPSEND=0,0,"Hello World"  //发送第一个字符串
+IPSEND: 0,11
OK
+IPRD: 0,11,Hello World  //收到原路返回的信息

>>>>>>>>>>  AT+IPSEND=0,0,"Second Time"  //发送第二个符串
+IPSEND: 0,11
OK
+IPRD: 0,11,Second Time  //收到原路返回的信息

>>>>>>>>>>  AT+IPCLOSE=0  //关闭socket
OK
;

© 2018 - IOT小分队文章发布系统 v0.3