论一人做项目的压力与收获

  • 2019 年 10 月 6 日
  • 筆記

论一人做项目的压力与收获

0.导论

大家好,终于到了周末,有时间来做个总结,来跟大家一起来分享与学习,最近一直在做项目,除此之外,做点其他事情,并没有时间去分享公众号文章。今天主要来谈谈一人做项目的压力与收获以及从一个项目中如何去学习以及有什么样的压力的问题。

最近做的项目的内容会在下文给大家分享,而关于如何学以及如何应对问题,压力等,欢迎各位在留言区讨论。

这个项目做了将近1个月,项目的语言为c,主要做防火墙功能实现,在实际应用中有非常大的价值。在这期间,导师每隔2天左右,会催促一次进度,而在这个过程中,就面临非常大的压力,因为从来没有去做过如此陌生的项目以及时间限制等等。但是实际上,正是因为他的催促,让我学到了很多,也正是因为催促,才让我对项目的认识逐渐明朗。

一开始的要求就是直接去实现两台机器的ping通,在机器之间ping通看似非常简单的一件事,但是当你加入两个服务器之后,再去ping通,这个就并非一件简单的事情,需要自己写很多代码,还有非常重要的一点,需要非常熟悉计算机网络,而对于我,考研当时考得学硕,并没有计算机网络,本科学得也很菜,对于以太网帧的认识是不够的,特别是如何去解析协议,每个协议的底层又是什么,这都是要学得东西,另外这次不是所谓的纯理论,而是让你去定义这些协议结构体,并且去实现它,这一连串的压力也就来了,关于这些压力,一个人在做这个项目,并没有人进行沟通,只能跟谷歌进行face to face,这也就是我面对压力的一种方式吧。一开始觉得非常难,当了解过后这些基础内容后,就需要做各种测试,而测试非常重要。

举个测试例子,在本机跑代码完全没问题,直接放到两台服务器上,各种错误泪奔。为什么会这样?那是因为当把多个模块集成到一块的时候,你的思路有可能顾不上某一块点,所以一头雾水,而对于当时开发的我来说,就只有分开各个模块,多单元测试等,来进行调试,这一步非常重要。

关于开发中的调试,在c中是众所周知的gdb,这个要非常熟悉,通过-g,编译定位错误行数,通过断点调试来获取精准定位,对于分析问题至关重要。

以上就是开发中需要关注的两点,测试调试。好了,接下来,一起来看这次项目的干货,以及期待大家的留言分享

1.整体框架

2台笔记本在2台服务器之间做透明传输。

防火墙规则设置白名单,对于ARP协议无条件过滤,对于IP协议则筛选相关协议,支持TCP、UDP、ICMP,其余全部不进行传输。

关于TCP、UDP则设置目的端口作为过滤条件,对于ICMP则设置源IP地址作为过滤条件。

防火墙部分采用国密算法,支持5种加密模式,分别时:ECB、CBC、CFB、OFB、CTR。

现阶段完成ICMP、TCP、UDP防火墙过滤规则设置,后台读取Mysql数据库进行过滤,前台读取Mysql进行增删改,并设定全局加密规则,允许设定是否加密、加密模式选择、加解密密钥设置。

现阶段存在的问题,使用scp下载文件速度慢,需要做进一步的提升。

测试版本的代码行数如下:共计1615行c代码。(不算加密算法与其他版本)。

2.具体实现

项目框架实际上是一个往返过程,比如net1->usb1->usb2->net2,反之则是net2->usb2->usb1->net1,对此过程进行抽象,则可以抽取出多个线程,分别是net->usb线程与usb->net线程。针对于此,本次项目开发,采用多线程开发,只需要在两台服务器上运行相同的代码即可完成任务。

2.1 线程核心问题

在c语言中,由于在本次开发中需要传递libusb句柄及其他设备参数,所以需要考虑的第一个问题就是:线程中如何传递多参数。另外,由于开启了两个线程,那么如何保证多个线程不被主线程强制退出

传递参数

现在来第一个问题:传递参数

首先来看一下线程函数。

#include <pthread.h>  //引入头文件    int pthread_create(pthread_t *restrict tidp,                     const pthread_attr_t *restrict attr,                     void *(*start_rtn)(void),                     void *restrict arg);  

第一个参数为指向线程标识符的指针。第二个参数用来设置线程属性。第三个参数是线程运行函数的起始地址。最后一个参数是运行函数的参数。

根据参数的解析,知道传递参数是通过最后一个参数来传递的!

在实际项目代码中体现如下:

pthread_t netToUsbId; //创建线程ID    struct dataDirection netToUsbPara=readMysql(conn_ptr);  ...  ...  ...  /*      传递参数(&netToUsbPara)  */  pthread_create(&netToUsbId,NULL,netToUsb_Thread,&netToUsbPara);      void * netToUsb_Thread(void * args){      //线程参数获取,数据流动方向      struct dataDirection *directPara;      directPara = (struct dataDirection *) args; //获取参数      libusb_device_handle *dev_handle = directPara->dev_handle;      ...      ...      ...  }  

同理,usb->net同上实现。

注意点:在多线程写完程序后,gcc编译需要加上-lpthread,例如:

gcc -o pthread pthread.c -lpthread  

多线程

对于第二个问题,则需要使用pthread_join函数。

pthread_join使一个线程等待另一个线程结束。代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。

所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。

函数原型:

int pthread_join(pthread_t thread, void **retval);    形参:      thread     被等待的线程标识符      retval     一个用户定义的指针,它可以用来存储被等待线程的返回值    返回值:成功返回0,否则返回错误的编号  

在本次项目中则是通过如下方式保证多个线程正常运转:

pthread_create(&netToUsbId,NULL,netToUsb_Thread,&netToUsbPara);  pthread_create(&usbToNetId,NULL,usbToNet_Thread,&netToUsbPara);  pthread_join(netToUsbId,NULL);  pthread_join(usbToNetId,NULL);  

2.2 线程处理

两个线程分别是net->usb线程与usb->net线程。分别处理了什么事情?

net->usb

这个线程完成了从rawsocket接收数据,中间进行数据过滤并将数据加密,通过转发到usb端。

usb->net

这个线程完成了从usb端接收,中间进行数据解密操作,然后转发到net端。

3.日志打印

在测试传输速度时候,需要将代码运行从调试模式改为正常运行模式,也就是去掉所有log打印,而在之前的开发中,全程使用printf来进行打印,如果每一个都进行注释,那么将是一个巨大的工作,于是就有了日志打印封装实现。

日志打印采用DEBUG来设定log的开关,独立成LogUtils.h文件。

封装如下:

#ifndef _LOG_UTILS_H_  #define _LOG_UTILS_H_    #include <stdio.h>  #include <string.h>    #define DEBUG // log开关    #define __INFO__ "info"  #define __WARN__ "warn"  #define __ERR__  "err"  #ifdef DEBUG    #define LOGI(format, ...) printf(format "n", ##__VA_ARGS__)  #define LOGW(format, ...) printf("[%s][%s][%s][%d]: " format "n",__WARN__ , __FILE__, __FUNCTION__,                              __LINE__, ##__VA_ARGS__)  #define LOGE(format, ...) printf("[%s][%s][%s][%d]: " format "n",__ERR__ , __FILE__, __FUNCTION__,                              __LINE__, ##__VA_ARGS__)    #define LOGIN(format, ...) printf(format,##__VA_ARGS__)    #else  #define LOGI(format, ...)  #define LOGW(format, ...)  #define LOGE(format, ...)  #define LOGIN(format, ...)  #endif    #endif  

DEBUG行被注释掉,在代码中所调用的打印操作便全部失效,也就是注释掉后,执行上述的else语句。

4.c操作数据库Mysql

引入头文件

#include "mysql.h"  

指针初始化

MYSQL *conn_ptr;  conn_ptr = mysql_init(NULL);//初始化  

连接数据库

conn_ptr = mysql_real_connect(conn_ptr, "localhost", "root", "123456","fire", 0, NULL, 0);//连接数据库 1则success or failed  

关闭数据库

mysql_close(conn_ptr);  

完整实现

#include <stdlib.h>  #include <stdio.h>  #include "mysql.h"  int main(int argc, char *argv[]) {     MYSQL *conn_ptr;       conn_ptr = mysql_init(NULL);//初始化     if (!conn_ptr) {        fprintf(stderr, "mysql_init failedn");        return EXIT_FAILURE;     }       conn_ptr = mysql_real_connect(conn_ptr, "localhost", "root", "123456","fire", 0, NULL, 0);//连接数据库 1则success or failed       if (conn_ptr) {        printf("Connection successn");     } else {        printf("Connection failedn");     }       mysql_close(conn_ptr);       return EXIT_SUCCESS;  

编译运行

gcc -o mysqltest mysqltest.c -I/usr/include/mysql -L/usr/lib/mysql -lmysqlclient  

上述为c打开关闭数据库的完整流程,那么如何进行数据库表的查询呢?

在项目中第一个版本代码中使用了mysql_query函数,后来改为两张表查询,所以就改为mysql_real_query函数。

两者究竟有什么区别呢?

mysql_query不能用于语句包含二进制数据,必须使用mysql_real_query代替。(二进制数据可能包含“ 0”的性质,这mysql_query()作为字符串结束的声明解释。)

此外,mysql_real_query比快mysql_query,因为它不调用strlen

最后在项目中实际读取核心代码如下:

struct dataDirection readMysql(MYSQL *conn_ptr){      struct dataDirection para;      MYSQL_RES *res_ptr;      MYSQL_ROW sqlrow;      MYSQL_FIELD *fd;        char *query_str = NULL;        int res1, res2, i;      int rows;        char key[50];      int format_res;        if (!conn_ptr) {          LOGE("%d",EXIT_FAILURE);      }      conn_ptr = mysql_real_connect(conn_ptr, "localhost", "root", "xxxx", "firedb", 0, NULL, 0);      if (conn_ptr) {            query_str = "select * from firewall_filter";            res1 = mysql_real_query(conn_ptr, query_str, strlen(query_str)); //查询语句            // filter表读取          if (res1) {              LOGI("SELECT error:%s",mysql_error(conn_ptr));          } else {              res_ptr = mysql_store_result(conn_ptr);             //取出结果集              if(res_ptr) {                  LOGI("%lu Rows",(unsigned long)mysql_num_rows(res_ptr));                    rows = (unsigned long)mysql_num_rows(res_ptr);                  para.whitelen=rows;                    /*                      结构体过滤参数设置略                  */                    if (mysql_errno(conn_ptr)) {                      fprintf(stderr,"Retrive error:sn",mysql_error(conn_ptr));                  }              }          }            query_str = "select * from firewall_encrypt";          res2 = mysql_real_query(conn_ptr, query_str, strlen(query_str)); //查询语句              // filter表读取          if (res2) {              LOGI("SELECT error:%s",mysql_error(conn_ptr));          } else {              res_ptr = mysql_store_result(conn_ptr);             //取出结果集              if(res_ptr) {                  LOGI("%lu Rows",(unsigned long)mysql_num_rows(res_ptr));                    rows = (unsigned long)mysql_num_rows(res_ptr);                  /*                      结构体加密设置略                  */                  if (mysql_errno(conn_ptr)) {                      fprintf(stderr,"Retrive error:sn",mysql_error(conn_ptr));                  }              }              mysql_free_result(res_ptr);          }            if(res2&&!res1){              mysql_free_result(res_ptr); //避免指针未被free掉的问题          }      } else {          LOGI("Connection failedn");      }      mysql_close(conn_ptr);      return para;  }  

5.各种错误

在本次项目中,出现了一个非常大的问题,就是协议未能正常解析,各种段错误。

段错误

当以普通用户执行代码的时候,直接出现段错误,因为没有相应的usb初始化权限,所以导致指针的误操作,也就出现了段错误。

协议未能正常解析

第一种:未采用1字节对齐。明显的错位导致协议未能正常解析!

第二种:函数滥用。在做加密算法的时候用到strcpy函数后,协议就不能正常解析了,此时有两种情况,第一加密与解密不正常。第二就是现在的这个函数滥用,当换成memcpy后,一切正常。

strcpymemcpy的区别:

strcpy的原型:

char *strcpy(char *dest, const char *src)    

strcpy是拷贝字符串。

①不指定长度。

②以为标志结束(即一旦遇到''拷贝过程即停止)。

③只能拷贝字符串。

memcpy的原型:

void *memcpy(void *dest, const void *src, size_t n);  

memcpy是内存拷贝函数。

memcpy是给定来源和目标后,拷贝指定大小n个字节的内存数据。

②不会在处停下来。

③拷贝的内容不仅限于字符串。

对比上述两个函数后,就发现了自己的问题了,因为拷贝的内容不是普通字符串,遇到就停止了,所以就解析不正常了。

message too long

当接收数据时,数据太多。导致scp下载不了大文件!解决策略为修改MTU。

MTU:通信术语 最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。

MTU值默认为1500,而实际传递的数据比这个要大,所以就报message too long 。

如何修改MTU:

第一种方式:直接通过ifconfig命令即可修改。

ifconfig ${Interface} mtu ${SIZE} up  ifconfig eth1 mtu 1600 up  

第二种方式:修改配置文件。

CentOS / RHEL / Fedora Linux下

# vi /etc/sysconfig/network-scripts/ifcfg-eth0  #增加如下内容MTU="9000"  #保存后重启网卡生效  # service network restart  #启用IPv6地址的,修改IPv6 mtu的参数为IPV6_MTU="1280"  

Debian / Ubuntu Linux下

# vi /etc/network/interfaces  #增加如下值mtu 9000  #保存后,重启网络生效  # /etc/init.d/networking restart  

libusberr

错误原因:USB接受buff与发送的buff设置一样的大小,实际上在发送端发送出去的buff是加密后的数据,那么接受端要比发送的buff大,所以将接受端调大即可。

学习文章

1.linux下修改mtu值

2.memcpy和strcpy的区别