作者:陈广
日期:2021-4-29
之前在学BC26的时候写过Socket通信,只是现在芯片换了,指令也变了,需要重写。之前的文章需要有自己的服务器方能完成操作,现在这篇文章针对班上的学生,上课的时候自己架一个服务器所有人可以同时使用,所以服务器端也需要重写。另外,之前用的是.NET Core 2.0,现在也更新为.NET Core 5.0了,所以服务器的架设也需要重新来一遍。本次实验基于腾讯云服务器,操作系统为CentOS 7.5 64位版本。
在安装 .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
使用以下命令安装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 测试!
万事俱备,终于可以发数据了。首先介绍几个TCP相关的AT指令。这部份内容翻译自AT命令手册,在学习的时候,有些命令如果不太清楚使用方法,最好的办法就是阅读命令手册,足够详细,只是全英文对部分同学来说有些困难。
此命令创建一个TCP或UDP socket并连接至远程服务器。
语法:
设置命令:
AT+IPSTART=<sockid>,<type>,<addr>,<port>[,<cid>[,<domian>[,<protocol>]]]
参数:
<sockid>
:整数,socket信道号,范围0-4。也就是说,最多可以同时运行5个socket。<type>
:字符串
<addr>
:字符串,远程地址。此命令不会进行合法性检查,必须确保输入的IP地址合法。<port>
:整数,远程端口。<cid>
:整数PDP上下文ID,由AT+EGACT
命令响应。<domain>
:整数,默认值为 2。其中:
<protocol>
:发送的数据包数量,默认值为0,现今仅支持0
此命令用于向网络发送数据,如果响应“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>
:整数,优先级标志。
<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>
,该条指令将往所配置的地址和端口发送数据,该地址仅对此命令生效一次;此命令用于设置socket的接收配置。
语法:
+IPRCFG=<auto_receive>[,<mode>[,<hex>]]
如果成功:
OK
如果失败:
ERROR
参数:
<auto_receive>
:整数,为1时,当数据到达,直接输出到AT端口;为0时,则需要使用 +IPRD 命令手动读取数据。<mode>
:整数,数据显示的格式:
+IPRD: <socket_id>,<data_len>,<data>
<data>
+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
指示从网络接收到一些数据。
语法:
+IPNMI: <socket_id>,<data_len>
参数:
<socket_id>
:整数,socket信道号<data_len>
:整数,传入数据的长度。如果使用 +IPRD 命令读取的长度小于<data_length>
,几秒后如果没有再次读取,则将再次收到此命令指示剩余数据。此命令用于手动读取socket数据
语法:
+IPRD=<socket_id>,<data_length>
如果成功:
OK
如果失败:
ERROR
参数:
<socket_id>
:整数,socket信道标识<data_length>
:整数,需要读取的数据长度,如果实际接收的数据小于<data_length>
,在 +IPRD 响应中会返回实际接收的数据长度。数据长度范围是1-1440字节。此命令用于断开并关闭socket。如果socket为TCP,它将开始发送TCP FIN数据包;如果socket为UDP,将不会发送数据包。
语法:
+IPCLOSE=<socket_id>
成功返回:
OK
失败返回:
ERROR
参数:
<socket_id>
:整数,socket ID。接下来可以深度连接服务器收发数据:
>>>>>>>>>> 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和使用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即可。
>>>>>>>>>> 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
;