网桥+tap+epoll实现交换机

网桥+tap+epoll实现交换机

本方案需要两个物理网卡,且每个网卡都需要网桥连接到Tap虚拟网卡,此时可以通过向Tap虚拟网卡进行读写操作达到对物理网卡进行读写操作。

网络拓扑实现过程

网络拓扑实现过程如下:

  1. 使用tunctl命令创建2个Tap虚拟网卡
  2. 使用brctl addbr命令创建2个网桥
  3. 使用brctl addif命令将创建好的虚拟网卡与物理网卡进行连接
  4. 启用网桥和虚拟网卡,达到不暴露物理网卡的目的

将命令写成shell脚本,如下:

tunctl
tunctl
brctl addbr br0
brctl addif br0 tap0
brctl addif br0 ens37
brctl addbr br1
brctl addif br1 tap1
brctl addif br1 ens38
brctl show
ifconfig tap0 up
ifconfig tap1 up
ifconfig br1 up 
ifconfig br0 up
ifconfig

程序实现流程(C语言版)

打开Tap虚拟网卡

首先,需要有一个函数新建Tap虚拟网卡或将现有的虚拟网卡打开,代码如下:

int tap_create(char *dev, int flags)
{
    struct ifreq ifr;
    int fd, err;


    assert(dev != NULL);

    if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
        return fd;

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags |= flags;
    if (*dev != '\0')
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) 
    {
        close(fd);
        return err;
    }
    
    strcpy(dev, ifr.ifr_name);

    return fd;
}

接下来我们要在main函数里面将我们的脚本中创建的虚拟网卡在程序中打开:

char tap0_name[IFNAMSIZ] = "tap0";
char tap1_name[IFNAMSIZ] = "tap1";
tap0 = tap_create(tap0_name, IFF_TAP | IFF_NO_PI);

if (tap0 < 0) 
{
    perror("tap_create");
    return 1;
}
printf("TUN name is %s\n", tap0_name);

tap1 = tap_create(tap1_name, IFF_TAP | IFF_NO_PI);

if (tap1 < 0) 
{
    perror("tap_create");
    return 1;
}
printf("TUN name is %s\n", tap1_name);
创建Epoll事件

首先在main函数中,创建一个epoll句柄,并判断是否创建成功

int efd;
efd = epoll_create(MAXEVENTS);
if (efd == -1)
{
	perror ("epoll_create");
	abort ();
}

之后,使用epoll的注册函数监听两个虚拟网卡,监听对应的文件描述符可以读,并将epoll设为水平触发模式(分为水平触发模式(LT)和边沿触发模式(ET)),两种触发模式区别如下:

LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。

代码如下:

int s;
struct epoll_event event;
struct epoll_event *events;
event.data.fd = tap0;
event.events = EPOLLIN;// 读入,水平触发方式(默认)
s = epoll_ctl (efd, EPOLL_CTL_ADD, tap0, &event);
if (s == -1)
{
	perror ("epoll_ctl");
	abort ();
}

event.data.fd = tap1;
event.events = EPOLLIN;// 读入,水平触发方式(默认)
s = epoll_ctl (efd, EPOLL_CTL_ADD, tap1, &event);
if (s == -1)
{
	perror ("epoll_ctl");
	abort ();
}

之后,需要为缓存数据申请一块空间:

events = calloc (MAXEVENTS, sizeof event);
监听Epoll事件

在创建并添加完epoll事件后,需要使用while循环不停监控是否有事件发生:

while(1)
{
	int n, i;
	n = epoll_wait (efd, events, MAXEVENTS, -1);
	for (int i = 0; i < n; i++)
	{
		/* ... */
	}
}

当发现有事件发生之后,要在for循环中添加代码判断发生事件的编号,执行相应处理,还要加上一个排错程序:

if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)))
{
	/* 这个fd上发生了一个错误,或者套接字还没有准备好读取*/
	fprintf (stderr, "epoll error\n");
	close (events[i].data.fd);
	continue;
}
else if (tap0 == events[i].data.fd)
	{
		printf("Hello ");
    int ret;
    unsigned char buf[4096];
    ret = read(tap0, buf, sizeof(buf));
    if(ret < 0)
    {
	    continue;
    }
    int i = 0;
    printf("Receive from tap0!\n");
    if(ret > 0)
    {
    	ret = write(tap1, buf, ret);
    }
    
}
else if (tap1 == events[i].data.fd)
{
	printf("Hello ");
    int ret;
    unsigned char buf[4096];
    ret = read(tap1, buf, sizeof(buf));
    if(ret < 0)
    {
	    continue;
    }
    int i = 0;
    printf("Receive from tap1!\n");
    if(ret > 0)
    {
    	ret = write(tap0, buf, ret);
    }
    
}
else
{
	continue;
}

最后一定要记得关闭之前添加的事件,并把申请的缓存空间释放

free (events);
close (tap0);
close (tap1);

程序实现流程(Python版)

打开Tap虚拟网卡

创建一个函数用于新建或打开一个虚拟网卡:

def tun_create(devname, flags):
	fd = -1
	if not devname:
		return -1
	fd = os.open("/dev/net/tun", os.O_RDWR)
	if fd < 0:
		print("open /dev/net/tun err!")
		return fd
	r=IfReq()
	ctypes.memset(ctypes.byref(r), 0, ctypes.sizeof(r))
	r.ifr_ifru.ifru_flags |= flags
	r.ifr_ifrn.ifrn_name = devname.encode('utf-8')
	try:
		err = fcntl.ioctl(fd, TUNSETIFF, r)
	except Exception as e:
		print("err:",e)
		os.close(fd)
		return -1
	return fd

之后打开之前创建的虚拟网卡:

tap0 = tun_create("tap0", IFF_TAP | IFF_NO_PI)
	if tap0 < 0:
		raise OSError

	tap1 = tun_create("tap1", IFF_TAP | IFF_NO_PI)
	if tap1 < 0:
		raise OSError
创建Epoll对象并注册虚拟网卡读事件

首先创建Epoll对象

epoll = select.epoll()       #: 创建一个epoll对象

之后,使用epoll的注册函数监听两个虚拟网卡,监听对应的文件描述符可以读,并将epoll设为水平触发模式

epoll.register(tap0, select.EPOLLIN)
epoll.register(tap1, select.EPOLLIN)
监听Epoll事件

创建一个while循环,不停询问是否有事件发生,当有事件发生时按照事件名进行对应操作:读取文件描述符中的数据(即改网卡接收到的数据),并将数据写入另一个网卡的文件描述符(即使用另一个网卡发送出去):

while True:
	events = epoll.poll(1)
	for fd, event in events:
		if tap0 == fd:
			buf0 = os.read(tap0, 4096)
			print("read from tap0 size:%d" % len(buf0))
			ret = os.write(tap1, bytes(buf0))
			# print(str(ret) + " " + buf0)

		if tap1 == fd:
			buf1 = os.read(tap1, 4096)
			print("read from tap1 size:%d" % len(buf1))
			ret = os.write(tap0, bytes(buf1))
			# print(str(ret) + " " + buf1)

最后要关闭之前添加的事件:

epoll.unregister(tap0)
epoll.unregister(tap1)
epoll.close()

多线程、多进程、Epoll实现交换机速度与连通性对比

到这里就完成了整个程序。接下来需要对多线程、多进程、Epoll这3个实现交换机功能的方案进行速度对比。

  • 测试环境:2台win10电脑(IP后8位分别为177和186)均使用千兆网卡与网线
  • 测试工具:Ping、 SocketTool、 飞鸽传书速度
  • 测试项:Ping延时、SocketTool分别模拟TCP和UDP时连通性、飞鸽传书传输文件速度

测试结果如下:

测试项 SocketTool(UDP)Server->Client SocketTool(UDP)Client->Server SocketTool(TCP)Server->Client SocketTool(TCP)Client->Server Ping延时 飞鸽传书速度 39->186 飞鸽传书速度 186->39
Epoll© 正常 正常 正常 正常 1ms 90m/s 80m/s
多线程 正常 正常 正常 正常 1ms 83m/s 80m/s
多进程 正常 正常 正常 正常 1ms 85m/s 80m/s
Epoll(Python) 正常 正常 正常 正常 1ms 85m/s 86m/s

附:完整C程序

#include <linux/if_tun.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <stdlib.h>

#define MAXEVENTS 64

int tap_create(char *dev, int flags)
{
    struct ifreq ifr;
    int fd, err;


    assert(dev != NULL);

    if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
        return fd;

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags |= flags;
    if (*dev != '\0')
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) 
    {
        close(fd);
        return err;
    }
    
    strcpy(dev, ifr.ifr_name);

    return fd;
}

int main(int argc, char *argv[])
{
    int tap0, tap1, ret;
    char tap0_name[IFNAMSIZ] = "tap0";
    char tap1_name[IFNAMSIZ] = "tap1";

	int efd;
	int s;
	struct epoll_event event;
	struct epoll_event *events;

    tap0 = tap_create(tap0_name, IFF_TAP | IFF_NO_PI);

    if (tap0 < 0) 
    {
        perror("tap_create");
        return 1;
    }
    printf("TUN name is %s\n", tap0_name);

    tap1 = tap_create(tap1_name, IFF_TAP | IFF_NO_PI);

    if (tap1 < 0) 
    {
        perror("tap_create");
        return 1;
    }
    printf("TUN name is %s\n", tap1_name);

    efd = epoll_create(MAXEVENTS);
    if (efd == -1)
    {
		perror ("epoll_create");
		abort ();
    }

	event.data.fd = tap0;
	event.events = EPOLLIN;// 读入,水平触发方式(默认)
	s = epoll_ctl (efd, EPOLL_CTL_ADD, tap0, &event);
	if (s == -1)
	{
		perror ("epoll_ctl");
		abort ();
	}

	event.data.fd = tap1;
	event.events = EPOLLIN;// 读入,水平触发方式(默认)
	s = epoll_ctl (efd, EPOLL_CTL_ADD, tap1, &event);
	if (s == -1)
	{
		perror ("epoll_ctl");
		abort ();
	}

	/* Buffer where events are returned */
	events = calloc (MAXEVENTS, sizeof event);

	while(1)
	{
		int n, i;
 		n = epoll_wait (efd, events, MAXEVENTS, -1);
 		for (i = 0; i < n; i++)
 		{
 			if ((events[i].events & EPOLLERR) ||
              (events[i].events & EPOLLHUP) ||
              (!(events[i].events & EPOLLIN)))
 			{
 				/* An error has occured on this fd, or the socket is not
                 ready for reading (why were we notified then?) 
                 这个fd上发生了一个错误,或者套接字还没有准备好读取(为什么当时通知了我们?)*/
				fprintf (stderr, "epoll error\n");
				close (events[i].data.fd);
				continue;
 			}
 			else if (tap0 == events[i].data.fd)
 			{
 				printf("Hello ");
			    int ret;
			    unsigned char buf[4096];
		        ret = read(tap0, buf, sizeof(buf));
		        if(ret < 0)
		        {
				    continue;
			    }
			    int i = 0;
			    printf("Receive from tap0!\n");
/*
			    for(i=0; i<ret; i++)
			    {
				    printf("%x", buf[i]);
			    }
*/
			    if(ret > 0)
		        {
		        	ret = write(tap1, buf, ret);
			    }
			    
			}
			else if (tap1 == events[i].data.fd)
 			{
 				printf("Hello ");
			    int ret;
			    unsigned char buf[4096];
		        ret = read(tap1, buf, sizeof(buf));
		        if(ret < 0)
		        {
				    continue;
			    }
			    int i = 0;
			    printf("Receive from tap1!\n");
/*
			    for(i=0; i<ret; i++)
			    {
				    printf("%x", buf[i]);
			    }
*/
			    if(ret > 0)
		        {
		        	ret = write(tap0, buf, ret);
			    }
			    
			}
			else
			{
				continue;
			}
 			
 		}
	}
	free (events);

	close (tap0);
	close (tap1);

	return EXIT_SUCCESS;
}


附:完整Python程序

# -*- encoding:utf-8 -*-
import fcntl
import os
import sys
import select
import socket
import ctypes
import struct

from if_tun import IfReq, TUNSETIFF, IFF_TUN, IFF_TAP, IFF_NO_PI

DEF_BUF=[0xff,0xff,0xff,0xff,0xff,0xff,
		 0x2a,0x5e,0xc0,0xab,0xdc,0xae,
		 0x08,0x00,
		 0x01,0x02,0x03,0x04,0x05,0x06,
		 0x01,0x02,0x03,0x04,0x05,0x06,
		 0x01,0x02,0x03,0x04,0x05,0x06,
		 0x01,0x02,0x03,0x04,0x05,0x06,
		 0x01,0x02,0x03,0x04,0x05,0x06,
		 0xff,0xff]
BUF=bytes(DEF_BUF)

ETHER_BROAD_ADDR=[0xff,0xff,0xff,0xff,0xff,0xff]
MIN_EHTER_PKG_LEN=46

def tun_create(devname, flags):
	fd = -1
	if not devname:
		return -1
	fd = os.open("/dev/net/tun", os.O_RDWR)
	if fd < 0:
		print("open /dev/net/tun err!")
		return fd
	r=IfReq()
	ctypes.memset(ctypes.byref(r), 0, ctypes.sizeof(r))
	r.ifr_ifru.ifru_flags |= flags
	r.ifr_ifrn.ifrn_name = devname.encode('utf-8')
	try:
		err = fcntl.ioctl(fd, TUNSETIFF, r)
	except Exception as e:
		print("err:",e)
		os.close(fd)
		return -1
	return fd

if __name__ == "__main__":	
	tap0 = tun_create("tap0", IFF_TAP | IFF_NO_PI)
	if tap0 < 0:
		raise OSError

	tap1 = tun_create("tap1", IFF_TAP | IFF_NO_PI)
	if tap1 < 0:
		raise OSError

	epoll = select.epoll()       #: 创建一个epoll对象
	epoll.register(tap0, select.EPOLLIN)
	epoll.register(tap1, select.EPOLLIN)


	while True:
		events = epoll.poll(1)
		for fd, event in events:
			if tap0 == fd:
				buf0 = os.read(tap0, 4096)
				print("read from tap0 size:%d" % len(buf0))
				ret = os.write(tap1, bytes(buf0))
				# print(str(ret) + " " + buf0)

			if tap1 == fd:
				buf1 = os.read(tap1, 4096)
				print("read from tap1 size:%d" % len(buf1))
				ret = os.write(tap0, bytes(buf1))
				# print(str(ret) + " " + buf1)
				
	epoll.unregister(tap0)
	epoll.unregister(tap1)
	epoll.close()
上一篇:【Linux网络编程】Nginx -- 事件模块(三)


下一篇:I/O模型和Java NIO源码分析