● 实时修改多网口下位机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;
}
}
}
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
下位机目前为双网卡,均为静态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的办法有很多种:
- ifconfig命令+写入/etc/network/interfaces.d/配置文件。这个方法在修改eth1的时候是可行的,但是ifconfig修改eth0的IP造成网关不可达,一直未排查到原因;
- 还有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在同一网段。最后成品如下视频: