Ethernet - JSON Application#

准备#

网站服务器返回 json 数据和返回 text/html 没有多大不同,也是以 http 方式传输 http 报文。该报文有报头和正文两行,中间用空行隔开。该 json 可以文件方式存在服务器上,也可以临时计算产生,对于接收端来说,看起来相同。

硬件连接与软件设置#

Arduino UNO 连接 Ethernet 模块

电脑和 UNO Ethernet 模块都用以太网双绞线连到同一个交换机。

此时在电脑上用 ping 命令可以测试电脑和 UNO 的网络连通性。 ping 通的时候,Ethernet 上的3个小灯会有规律地闪烁,表明有数据在传输。

代码的功能是完成服务器的功使,使得 Arduino UNO 小板作为 Web (Json) 服务器 (http://10.0.0.1),用电脑上的浏览器去访问该址,得到 json 格式的相应。

作为客户端的电脑配置 IP 地址为 10.0.0.100,需要与 UNO 在同一个局域网。如果你的电脑只有一个有线网卡,实验期间你可能上不了因特网。

Ethernet Demo Code#

Ethernet - EthernetServer() - Arduino Reference

这节的代码来自于官方说明书,可作为模板使用,后面的代码均在此基础上修改。

下面是在原代码的基础上,根据实际场景不同,略有改编后的代码:

#include <SPI.h>
#include <Ethernet.h>

// network configuration.  gateway and subnet are optional.

 // the media access control (ethernet hardware) address for the shield:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };   // (1)
//the IP address for the shield:
byte ip[] = { 10, 0, 0, 1};                            // (2)
// the router's gateway address:
// byte gateway[] = { 10, 0, 0, 1 };
// the subnet:
// byte subnet[] = { 255, 255, 0, 0 };

// telnet defaults to port 23
// EthernetServer server = EthernetServer(23);

void setup()
{
  // initialize the ethernet device
  //Ethernet.begin(mac, ip, gateway, subnet);
  Ethernet.begin(mac, ip);                              // (3)


  // start listening for clients
  server.begin();                                       // (4)
}

void loop()
{
  // if an incoming client connects, there will be bytes available to read:
  EthernetClient client = server.available();           // (5)
  if (client == true) {
    // read bytes from the incoming client and write them back
    // to any clients connected to the server:
    server.write(client.read());                        // (6)
  }
}
  1. Ethernet 硬件模块没有 MAC 地址,可指定一个,值随意,满足 MAC 地址的规范即可;

  2. ip 地址改为了简短的 10.0.0.1, 网关和子网掩码用不到,注释掉了

  3. 以太网卡启动起来,并分配 IP 地址

  4. 启动 TCP/UDP Socket 服务

  5. 等待来自 client 的连接

  6. 进一步处理 ... 这一行的代码需要改写为自己想要的功能

JSON for VLAN#

和前一个代码相比,提供了返回 json 字符串的功能。

#include <SPI.h>
#include <Ethernet.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = { 0x00, 0xAA, 0xBB, 0xCC, 0xDA, 0x02 };
IPAddress ip(10,0,0,1); //<<< ENTER YOUR IP ADDRESS HERE!!!

// Initialize the Ethernet server library
// with the IP address and port you want to use 
// (port 80 is default for HTTP):
EthernetServer server(80);                // (1)

// int buttonPress = 1;

void setup()
{
  // pinMode(2, INPUT);

  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
}

void loop()
{
  // buttonPress = digitalRead(2);
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.0 200 OK");                // (2)
          // client.println("Content-Length: 215");  
          // client.println("Content-Type: text/html");
          client.println("Content-Type: application/json"); // (3)
          client.println("Connection: close");              // (4)
          client.println();                                 // (5)

          client.println("[{\"vlanid\": \"10\",\"vlanName\": \"one\"},");
          client.println("{\"vlanid\": \"20\",\"vlanName\": \"two\"},");
          client.println("{\"vlanid\": \"30\",\"vlanName\": \"three\"}]");

          client.println();

          client.stop();                                    // (6)
        }

      }
    }
  }
}
  1. 考虑到我们将用浏览器作为客端,选择了 80 端口,这样我们就可以在 url 中省略 80,方便一点。你也可以根据你的实际需要改为不同的端口

  2. 发送 http 响应报头。即正确收到了客户端的响应后,需要发送的响应。该报头连续多行,最后以一个空行结束。

  3. 设置响应报文的类型为 json 应用类型。在不作修改的默认情况下是 text/html

  4. 设置服务器在完成传输后,立即断开 TCP 连接。这可能不符合你的需求,只不过我希望发送往响应后立即断开,方便调试。实际这个设置并不起作用,因为断不断开,得靠服务器的进一步行为来确定。

  5. 以空行结束 HTTP 报头。

  6. 主动终止 TCP 连接。否则实际上 TCP 连接还是打开的,即光有头部的 connection: close 是不够的,此时浏览器就不知道数据接收完了没有,会一直刷新(转圈)。由服务器发送完数据后就断开连接,调试起来更省时间。

  7. 从 (5) 到 (6) 之间是显示 vlan id 的业务所需要的返回的 json 字符串。

此内容你也可以通过下面的网址看到,只不过因为服务器不同,报头是不同的: https://share-1304436219.cos.ap-beijing.myqcloud.com/api/vlan.json

你可以用 Firefox 浏览器分别访问这两个网址,查看结果和报头 (Header),比较它们的异同。甚至你还可以通过 Wireshark 抓包来研究 Arduino Ethernet 到底在网络上发送了哪些数据,更深入的理解 HTTP 到底做了啥。

VLAN add Ports#

下面的代码的业务是将端口加入 vlan 后返回的结果。因为所有的 json 都包括返回 http 报头,然后是业务数据这两块。为了主程序逻辑更清楚,也方便代码复用,我们将这两块代码写成函数,要实现其它功能时,只需修改业务子函数即可。

#include <SPI.h>
#include <Ethernet.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = { 0x00, 0xAA, 0xBB, 0xCC, 0xDA, 0x02 };
IPAddress ip(10, 0, 0, 1);  //<<< ENTER YOUR IP ADDRESS HERE!!!

// Initialize the Ethernet server library
// with the IP address and port you want to use
// (port 80 is default for HTTP):
EthernetServer server(80);

void setup() {

  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
}

void send_http_response_header(EthernetClient client){  // (1)
  // send a standard http response header
  client.println("HTTP/1.0 200 OK");
  client.println("Content-Type: application/json");
  client.println("Connection: close");
  client.println();
}

void send_json(EthernetClient client){                   // (2)
  client.println("{");
  client.println("\"action\": \"addPortsToVlan\",");
  client.println("\"vlanId\": 10,");
  client.println("\"ports\": [");
  client.println("    \"GigabitEthernet0/1\",");
  client.println("    \"GigabitEthernet0/2\",");
  client.println("    \"GigabitEthernet0/4\"");
  client.println("  ]");
  client.println("}");
}


void loop() {
  // buttonPress = digitalRead(2);
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          send_http_response_header(client);        // (3)
          send_json(client);                        // (4)
          client.println();
          client.stop();
        }

      }
    }
  }
}

此时,我们省去了和之前相重复的解释,将重点放在不同的代码上。

  1. 发送报头的子函数定义

  2. 发送报文正文(body)的子函数定义

  3. 调用发送报头的子函数

  4. 调用发送报文正文(body)的子函数

可以看出,此时的主程序逻辑简单得多。

需要进一步解决的问题#

  1. 根据不同的网址返回不同的 json

  2. 将多个(3个)不同的功能集成为一个主程序