● 实时修改多网口下位机IP地址

前言

想要实现一个功能:类似在路由器的界面中配置参数数据。

下位机操作系统是linux,http服务的方案选的是轻量级的boa+cgi。很后悔选这个方案,低估了学习c语言的难度,遇到了不少坑。如果选nginx+.net应该会快很多。

操作流程大致如下:

功能明细

boa提供http web服务,首页为一个静态页面index.html,通过ajax请求cgi动态接口返回和处理数据。

1. 列出所有网卡

接口为:/cgi-bin/get_interface.cgi

核心代码:

#include <sys/ioctl.h>
#include <net/if.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
 
int main()
{
    struct ifreq ifr;
    struct ifconf ifc;
    char buf[2048];
    int success = 0;
 
    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }
 
    ifc.ifc_len = sizeof(buf);
    ifc.ifc_buf = buf;
    if (ioctl(sock, SIOCGIFCONF, &ifc) == -1) {
        printf("ioctl error\n");
        return -1;
    }
 
    struct ifreq* it = ifc.ifc_req;
    const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq));
    char szMac[64];
    int count = 0;
    for (; it != end; ++it) {
        strcpy(ifr.ifr_name, it->ifr_name);
        if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) {
            if (! (ifr.ifr_flags & IFF_LOOPBACK)) { // don't count loopback
                if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) {
                    count ++ ;
                    unsigned char * ptr ;
                    ptr = (unsigned char  *)&ifr.ifr_ifru.ifru_hwaddr.sa_data[0];
                    snprintf(szMac,64,"%02X:%02X:%02X:%02X:%02X:%02X",*ptr,*(ptr+1),*(ptr+2),*(ptr+3),*(ptr+4),*(ptr+5));
                    printf("%d,Interface name : %s , Mac address : %s \n",count,ifr.ifr_name,szMac);
                }
            }
        }else{
            printf("get mac info error\n");
            return -1;
        }
    }
}

参考资料:《Linux下C语言获取所有网卡信息的代码》

2. 根据网卡获取IP、子网掩码、网关信息

接口为:/cgi-bin/get_info.cgi

/**
 * get IPv4 address and subnet mask of a network interface
 */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <arpa/inet.h>

int
main(int argc, char *argv[])
{
        int rc = 0;
        struct sockaddr_in *addr = NULL;

        if (argc != 2) {
                fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
                return -1;
        }

        char *ifname = argv[1];

        struct ifreq ifr;
        memset(&ifr, 0, sizeof(struct ifreq));

        /* 0. create a socket */
        int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
        if (fd == -1)
                return -1;

        /* 1. set type of address to retrieve : IPv4 */
        ifr.ifr_addr.sa_family = AF_INET;

        /* 2. copy interface name to ifreq structure */
        strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);

        /* 3. get the IP address */
        if ((rc = ioctl(fd, SIOCGIFADDR, &ifr)) != 0)
                goto done;

        char ipv4[16] = { 0 };
        addr = (struct sockaddr_in *)&ifr.ifr_addr;
        strncpy(ipv4, inet_ntoa(addr->sin_addr), sizeof(ipv4));

        /* 4. get the mask */
        if ((rc = ioctl(fd, SIOCGIFNETMASK, &ifr)) != 0)
                goto done;

        char mask[16] = { 0 };
        addr = (struct sockaddr_in *)&ifr.ifr_addr;
        strncpy(mask, inet_ntoa(addr->sin_addr), sizeof(mask));

        /* 5. display */
        printf("IFNAME:IPv4:MASK\n");
        printf("%s:%s:%s\n", ifname, ipv4, mask);

        /* 6. close the socket */
done:
        close(fd);

        return rc;
}

参考资料:在Linux上使用C语言编程获取IPv4地址及子网掩码

获取网关比较复杂,核心代码如下:

int readNlSock(int sockFd, char *bufPtr, int seqNum, int pId)
{
    struct nlmsghdr *nlHdr;
    int readLen = 0, msgLen = 0;

    do
    {
        //收到内核的应答
        if((readLen = recv(sockFd, bufPtr, BUFSIZE - msgLen, 0)) < 0)
        {
            perror("SOCK READ: ");
            return -1;
        }

        nlHdr = (struct nlmsghdr *)bufPtr;

        //检查header是否有效
        if((NLMSG_OK(nlHdr, readLen) == 0) || (nlHdr->nlmsg_type == NLMSG_ERROR))
        {
            perror("Error in recieved packet");
            return -1;
        } 

        if(nlHdr->nlmsg_type == NLMSG_DONE)
        {
            break;
        }
        else
        { 
            bufPtr += readLen;
            msgLen += readLen;
        }

        if((nlHdr->nlmsg_flags & NLM_F_MULTI) == 0)
        {
            break;
        }
    }
    while((nlHdr->nlmsg_seq != seqNum) || (nlHdr->nlmsg_pid != pId));

    return msgLen;
}


//分析返回的路由信息
void parseRoutes(struct nlmsghdr *nlHdr, struct route_info *rtInfo, char *gateway, char *eth)
{
    struct rtmsg *rtMsg;
    struct rtattr *rtAttr;
    int rtLen;
    char *tempBuf = NULL;
    struct in_addr dst;
    struct in_addr gate;
    tempBuf = (char *)malloc(100);
    rtMsg = (struct rtmsg *)NLMSG_DATA(nlHdr);

    // If the route is not for AF_INET or does not belong to main routing table
    //then return. 
    if((rtMsg->rtm_family != AF_INET) || (rtMsg->rtm_table != RT_TABLE_MAIN)) 
    {
        return;
    }

    rtAttr = (struct rtattr *)RTM_RTA(rtMsg);
    rtLen = RTM_PAYLOAD(nlHdr);

    for(;RTA_OK(rtAttr,rtLen);rtAttr = RTA_NEXT(rtAttr,rtLen))
    { 
        switch(rtAttr->rta_type) 
        {
            case RTA_OIF:
                if_indextoname(*(int *)RTA_DATA(rtAttr), rtInfo->ifName);
            break;
            case RTA_GATEWAY:
                rtInfo->gateWay = *(u_int *)RTA_DATA(rtAttr);
            break;
            case RTA_PREFSRC:
                rtInfo->srcAddr = *(u_int *)RTA_DATA(rtAttr);
            break;
            case RTA_DST:
                rtInfo->dstAddr = *(u_int *)RTA_DATA(rtAttr);
            break;
        }
    }

    dst.s_addr = rtInfo->dstAddr;
    if(strstr((char *)inet_ntoa(dst), "0.0.0.0") && strstr(rtInfo->ifName, eth))
    {  
        // printf("oif: %s\n", rtInfo->ifName);
        gate.s_addr = rtInfo->gateWay;
        sprintf(gateway, (char *)inet_ntoa(gate));
        // printf("gateway: %s\n", gateway);
        // gate.s_addr = rtInfo->srcAddr;
        // printf("src: %s\n", (char *)inet_ntoa(gate));
        // gate.s_addr = rtInfo->dstAddr;
        // printf("dst: %s\n", (char *)inet_ntoa(gate));
    }

    free(tempBuf);
    return;
}

int getGateway(char *gateway, char *eth)
{
    struct nlmsghdr *nlMsg;
    struct rtmsg *rtMsg;
    struct route_info *rtInfo;
    char msgBuf[BUFSIZE];
    int sock, len, msgSeq = 0;

    if((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0)
    {
        perror("Socket Creation: ");
        return -1;
    }

    memset(msgBuf, 0, BUFSIZE);
    nlMsg = (struct nlmsghdr *)msgBuf;
    rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);
    nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); // Length of message.
    nlMsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table.
    nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump.
    nlMsg->nlmsg_seq = msgSeq++; // Sequence of the message packet.
    nlMsg->nlmsg_pid = getpid(); // PID of process sending the request.

    if(send(sock, nlMsg, nlMsg->nlmsg_len, 0) < 0)
    {
        printf("Write To Socket Failed…\n");
        return -1;
    }

    if((len = readNlSock(sock, msgBuf, msgSeq, getpid())) < 0)
    {
        printf("Read From Socket Failed…\n");
        return -1;
    }

    rtInfo = (struct route_info *)malloc(sizeof(struct route_info));

    for(;NLMSG_OK(nlMsg,len);nlMsg = NLMSG_NEXT(nlMsg, len))
    {
        memset(rtInfo, 0, sizeof(struct route_info));
        parseRoutes(nlMsg, rtInfo, gateway, eth);
    }

    // printf("eth=%s\n", eth);
    // printf("gateway=%s\n", gateway);

    free(rtInfo);
    close(sock);
    return 0;
}

int main()
{         
    char buff[256];
    getGateway(buff);
    printf("gateway = %s\n",buff);    
    return 0;
}

参考资料:《linux C 获取网关代码实现》

3. 设置IP、子网掩码、网关

接口为:/cgi-bin/set_ip.cgi

IMG_4391

下位机目前为双网卡,均为静态IP,eth0接入局域网,eth1与设备连接。这个接口消耗了很长时间。难点主要有:

  • eth0/eth1有一个需要提供http web服务(暂定eth0),需要更改后立即生效,保存IP信息,尽量无需重启,且重启后IP参数仍旧生效。
  • PC电脑要访问网口eth0提供的http web服务,必须要与eth0在同一局域网。如果eth0修改IP网段,分两种情况:1、PC与下位机直连,则只需修改PC的IP与下位机同一网段即可;2、通过路由器连接,路由器网段也需要修改。方便起见,使用PC与eth0网口直连。

第1个难点,即时修改网口IP地址,无需重启,且重启后仍旧生效。修改IP的办法有很多种:

  1. ifconfig命令+写入/etc/network/interfaces.d/配置文件。这个方法在修改eth1的时候是可行的,但是ifconfig修改eth0的IP造成网关不可达,一直未排查到原因;
  2. 还有ip、nmcli命令等。经测nmcli命令可以修改eth0,而且设置的静态IP重启后仍然有效,参考资料《Linux 中的 nmcli 命令》。但是无法修改网口eth1,因为eth1是没有被NetworkManager托管的。但是可以使用别的方法,参考资料《(笔记)Linux下C语言实现静态IP地址,掩码,网关的设置》。这样修改IP前弹出一个对话框,询问网卡是否提供web服务(是eth0还是eth1),按照不同网口执行不同操作。

这样修改下位机IP的部分完成了。如果PC电脑与下位机的IP参数如下:

# PC电脑IP
192.168.0.1
# eth0
192.168.0.2
# eth1
192.168.1.1

修改eth1不影响eth0与PC电脑之间的通信。但是如果修改eth0的IP为192.168.2.2,则此IP生效后无法与原PC电脑通讯,需要手动将PC电脑IP进行更改,因为web网页无法更改PC电脑的IP。这里只能给这个网页套一个皮,在eth0的IP请求后,PC电脑IP也改成同一网段。这里使用C#+webview2进行winform和js之间的交互,使用C#修改PC电脑的IP地址,使PC电脑始终与eth0在同一网段。最后成品如下视频: