論一人做項目的壓力與收穫

  • 2019 年 10 月 6 日
  • 筆記

論一人做項目的壓力與收穫

0.導論

大家好,終於到了周末,有時間來做個總結,來跟大家一起來分享與學習,最近一直在做項目,除此之外,做點其他事情,並沒有時間去分享公眾號文章。今天主要來談談一人做項目的壓力與收穫以及從一個項目中如何去學習以及有什麼樣的壓力的問題。

最近做的項目的內容會在下文給大家分享,而關於如何學以及如何應對問題,壓力等,歡迎各位在留言區討論。

這個項目做了將近1個月,項目的語言為c,主要做防火牆功能實現,在實際應用中有非常大的價值。在這期間,導師每隔2天左右,會催促一次進度,而在這個過程中,就面臨非常大的壓力,因為從來沒有去做過如此陌生的項目以及時間限制等等。但是實際上,正是因為他的催促,讓我學到了很多,也正是因為催促,才讓我對項目的認識逐漸明朗。

一開始的要求就是直接去實現兩台機器的ping通,在機器之間ping通看似非常簡單的一件事,但是當你加入兩個伺服器之後,再去ping通,這個就並非一件簡單的事情,需要自己寫很多程式碼,還有非常重要的一點,需要非常熟悉電腦網路,而對於我,考研當時考得學碩,並沒有電腦網路,本科學得也很菜,對於乙太網幀的認識是不夠的,特別是如何去解析協議,每個協議的底層又是什麼,這都是要學得東西,另外這次不是所謂的純理論,而是讓你去定義這些協議結構體,並且去實現它,這一連串的壓力也就來了,關於這些壓力,一個人在做這個項目,並沒有人進行溝通,只能跟Google進行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後,一切正常。

strcpy與memcpy的區別:

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的區別

Tags: