上次學了一些C開發相關的工具,這次再配置一下VIM,讓開發過程更爽一些。 另外再學一些linux下網絡開發的基礎,好多人學C也是為了做網絡開發。
開發環境
首先得有個Linux環境,有時候家里機器是Windows,裝虛擬機也麻煩,所以還不如30塊錢 買個騰訊雲,用putty遠程練上去寫代碼呢。
我一直都是putty+VIM在Linux下開發代碼,好幾年了,只要把putty和VIM配置好,其實 開發效率挺高的。
買好騰訊雲后,裝個Centos,會分配個外網IP,然后買個域名,在DNSPod解析過去,就 可以用putty遠程登錄了,putty一般做如下設置。
- window\appearance\font setting:consolas 12pt , 設置字體
- window\translate\charset:utf-8 , 設置字符集
- window\selection\action of mouse buttons:windows .. , 設置可以用鼠標選擇文字
- window\line of scoreback:20000 ,設置可滾屏的長度
- connection\auto-login username:root, 設置自動登錄的用戶名
- connection\seconds of keepalive:10, 設置心跳,防止自動斷開
設置完成后把這個會話起個名字,比如叫qcloud,下次用的時候先加載,然后open 就可以了, 所有設置會保存起來。這樣配置后putty已經很好用了,但我們還可以搞成 自動登錄,不需要每次都輸入密碼。
- 在Linux下ssh-keygen -t rsa 生成密鑰對
- 把私鑰id_isa下載到用scp下載到windows並用puttygen加載並重新保存私鑰。
- 在windows下新建快捷方式輸入D:\soft\putty.exe -i D:\ssh\wawa.ppk -load "qcloud" 其中-i 指定私鑰位置,-load指定會話名稱,
下次雙擊快捷方式就登錄上去了,而且上面的設置都會生效。對了,putty和puttygen 要在官方下載哦。
VIM配置
首先安裝最新的VIM.
wget ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2 ./configure --prefix=/usr/local/vim --enable-multibyte --enable-pythoninterp=yes make make install
修改下~/.bashrc, 加入如下兩句,可以讓vim和vi指定成剛安裝的版本
alias vim='/usr/local/vim/bin/vim' alias vi='vim'
簡單配置下VIM,就可以開工了, 打開~/.vimrc,添加如下:
" 基本設置 set nocp set ts=4 set sw=4 set smarttab set et set ambiwidth=double set nu " 編碼設置 set encoding=UTF-8 set langmenu=zh_CN.UTF-8 language message zh_CN.UTF-8 set fileencodings=ucs-bom,utf-8,cp936,gb18030,big5,euc-jp,euc-kr,latin1 set fileencoding=utf-8
基本每一個VIM最少要配置成這樣,包括生產環境,前半拉主要是設置縮進成4個空格, 后半拉是設置編碼,以便打開文件時不會亂碼。
如果想開發時更爽一些,就得裝插件了,現在裝插件也很簡單,先裝插件管理工具 pathogen.vim, 如下
mkdir -p ~/.vim/autoload ~/.vim/bundle && curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim
然后安裝一個文件模版插件,一個代碼片段插件,一個智能體似乎插件就可以了,傻瓜式 的,如下
# 安裝vim-template cd ~/.vim/bundle git clone git://github.com/aperezdc/vim-template.git # 安裝snipmate git clone git://github.com/msanders/snipmate.vim.git cd snipmate.vim cp -R * ~/.vim # 安裝clang_complete yum install clang git clone https://github.com/Rip-Rip/clang_complete.git cd clang_complete/ make install
再在~/.vimrc里加入如下兩句
execute pathogen#infect()
syntax on
filetype plugin indent on
別的插件能不裝就不裝了吧,用的時候再說,現在你打開一個新的.c文件,會自動從模版 里加載一個代碼框架進來,然后輸入main,for,pr等按tab鍵就會自動生成代碼片段, 然后include頭文件后,里面的函數,類型等在輸入時按ctrl+n就會自動提示,結構的 成員也可以,已經很爽了。
TCP基礎
TCP使用很廣泛,先了解一下概念,TCP是面向連接的協議,所以有建立連接和關閉連接 的過程。
建立連接過程需要三步握手,如下:
- A向B發送syn信令
- B向A回復ack,以及發送sync信令
- A向B回復ack
其實網絡上發送數據都有可能丟的,所以每個發送給對端的數據,要收到答復才能確認 對方收到了。 比如上面第二步A收到了B返回的ack才能確認連接已經建立成功,自己給B發送數據,B 可以收到,同樣第三步B收到A的ack才能確認連接建立成功,自己發給A的數據,A能收到。 所以TCP連接建立不是兩步握手,不是四步握手,而是三步握手。
連接建立成功后雙方就可以互發psh信令來傳輸數據了,同樣發出去的psh數據,也需要 收到ack才能確認對方收到,否則就得等待超時后重發。
拆除連接需要四步握手, 因為TCP是雙工的,所以自己這邊關閉連接,有可能對方還會 給自己發數據,還得等對方說自己不會給自己發送數據了。
- A向B發送fin, 表示自己沒有數據向B發送了。
- B向A回復ack
- B向A發送過fin, 表示自己沒有數據向A發送了。
- A向B回復ack
另外就是在任何時候都可能收到對方發來的rst信令,表示直接復位該連接,也別發數據了 也別等着收數據了,趕緊把資源都回收了吧。
TCP還有滑動窗口的流量控制機制,以及各種超時處理邏輯,有興趣的話具體細節看 《TCP/IP協議詳解》了。
linux下用tcpdump可以抓包學習TCP協議,比如在執行curl -I www.baidu.com
時用 tcpdump抓包如下。
# tcpdump -nn -t host www.baidu.com tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [S], seq 1772495094, win 14600, options [mss 1460,sackOK,TS val 214360452 ecr 0,nop,wscale 5], length 0 IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [S.], seq 946873815, ack 1772495095, win 14600, options [mss 1440,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 7], length 0 IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 1, win 457, length 0 IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [P.], seq 1:168, ack 1, win 457, length 167 IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 168, win 202, length 0 IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [P.], seq 1:705, ack 168, win 202, length 704 IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 705, win 501, length 0 IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [F.], seq 168, ack 705, win 501, length 0 IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 169, win 202, length 0 IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [F.], seq 705, ack 169, win 202, length 0 IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 706, win 501, length 0
可以看到本機的ip是10.190.176.177,baidu解析出來的ip是180.97.33.71,然后前三個 包就是建立連接的三步握手,最后三個包是關閉連接的四步握手。中括號里的S表示sync, p表示psh,F表示fin,.好像表示ack。
Linux基礎
其實Linux下,C的庫函數,以及linux API都在libc.so里面,沒有分開 的。玩C語言開發,肯定要對C庫函數和常用的linux API有所熟悉的,可以先看 如下兩個鏈接快速了解一下,知道系統有哪些能力和輪子。
Standard C 語言標准函數庫速查 http://ganquan.info/standard-c/ Linux系統調用列表http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html
再就是系統調用,Linux API, 系統命令,和內核函數不是一回事,雖然他們有關聯。 系統調用是通過軟中斷向內核提交請求,獲取內核服務的接口,Linux Api則定義了一組 函數如read,malloc等,封裝了系統調用, 比如malloc函數會調用brk系統調用。 然后有系統命令則更高一級,如ls,hostname,則直接提供了一個可執行程序, 關於他們 的關系可以閱讀下面這篇文章:
http://wenku.baidu.com/view/9e33f3e94afe04a1b071de81.html
C語言要想使用別人的東西,首先要包含別人提供的頭文件,使用linux api和c庫函數 也一樣,默認的這些頭文件都在/usr/include里,自己安裝的一些則一般約定放在 /usr/local/include里。寫代碼的過程中如果遇到一些類型或函數不知道怎么使用,直接 可以在這里面找到頭文件看源碼。
Linux下還有好多數據類型是在學普通C語言是沒見到過的,比如size_t,ssize_t,unit32_t 啥的, 這些其實都在普通數據類型的別名,一般在/usr/include/asm/types.h里可以看到 他們是怎么被typedef的,使用這些類型主要是為了提高可移植性,同時語義更加明確, 比如size_t在32位機器上定義為uint,64位機器上定義為ulong,使用size_t編寫的代碼 就可以在32位機器和64位機器上良好運行。 還有size_t的意義更明確,它不是用來表示 普通的無符號數字概念的,而是表示sizeof返回的結果或者說是能訪問的體系內存的長度。
然后像uint32_t這種類型是為了編寫出更明確的代碼,像C語言的類型,int, long等在 不同的機器上都有不同的長度,但uint32_t在啥機器上都是32位長的,有時候需求就是 這樣,就需要用這種數據類型了。
還有就是Linux系統函數調用失敗,大多數時候都會erron賦一個整數值,這個整數值可以 表示不同的錯誤原因,可以在終端下運行man errno來查看詳細,另外好多系統函數都可以 用man來查看幫助的,有的里面還有使用示例的,是學習linux編程的很好的工具。
還有一些系統函數設計的挺好,我總結了一些慣用法吧算是,自己設計函數也可以學習
第一個是通過指針參數來獲取數據,因為好多函數的返回值是int類型,表示函數調用 是否成功,或錯誤碼,而這個函數本身的任務還要返回一些實質的信息,這時候就可以 通過參數來填充數據,讓調用者拿到,比如accept函數的使用 (簡化后的偽代碼,不能執行):
struct sockaddr_in client; if (accept(listenfd, &client) >= 0) { printf("%s\n", client); }
這樣我們調用一次函數,既能知道有沒有調用成功,成功的話又能拿到客戶端的描述符, 以及對端的網絡地址。
第二個是C沒有類和對象的概念,但也可以模擬出來類似的概念,比如網絡編程,通過 socket函數創建一個描述符,比如說是fd,其實這就相當於一個類的實例,一個對象了, 然后調用read(fd),send(fd),close(fd)等函數來操作它,和面向對象里用fd.read(), fd.send(),fd.close()只是用法不同而已,所以寫C是能用得到一些面向對象的思想的。
第三個是在Linux里好多東西可以用描述符來表示,比如文件,硬件端口,網絡連接等, 然后可以針對描述符調用read,write等操作,這個是個很好的抽象,可以使用很簡單的 幾個接口來實現很強大的功能,在寫自己的C軟件時也可以借鑒這個思路。就是先建立一個 概念,然后寫很多的函數來操作這個概念,而不是建立很多的概念,大家記不住的。
第四個是,C其實沒有太多的類型檢查功能,表示復雜的數據都用struct表示,而不同的 struct是可以強轉的,所以可以用帶標志的struct來表達類似面向對象多態的概念,如 bind函數需要一個struct sockaddr的參數,但ipv4和ipv6的地址分別用 struct sockaddr_in和struct sockaddr_in6表示,感覺就相當於struct sockaddr的兩個 子結構,這樣bind函數就使用父結構struct sockaddr來同時支持ipv4和ipv6了。 需要注意子結構和父結構的標志成員要放在最前面,這樣子結構轉成父結構時,父結構 才能正確的讀出標志,從而在具體使用時強轉為合適的子結構。
就這樣了,Linux編程入門我知道的就這些,更多可看《Unix環境高級編程》
socket基礎
先學一些socket客戶端編程來熟悉socket編程吧, 要連接到遠程主機,首要要 有個遠程主機的地址,一個遠程主機的地址包含對方的IP和端口,有時候我們 只知道對方的域名,所以首先要解析出IP來,好多書上都是用gethostbyname來解析域名 的,但它過時了,不支持ipv6,而且參數不支持ip格式的字符串,返回的地址必須拷貝 后才能使用,否則同線程再調用一次該函數那地址就變了,總之是一個過時的函數了。
現在比較國際范的函數是getaddrinfo,可以通過man查它的用法,
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
該函數同時支持 ipv4和v6,然后host支持域名也支持ip格式的字符串,hints用來設置 查詢的一些條件,result用來獲取查詢到的結果,他是一個指向指針的指針類型。
這相當也是一個慣用法了,一個參數用來說明調用需求,一個指針參數來獲取返回數據。 像select就是調用需求和返回數據都是一個參數來表示,但像pool就是調用需求和返回 用兩個參數了,更明確,前一個是const,后一個是指針。具體使用示例如下:
struct addrinfo* get_addr(const char *host, const char *port){ struct addrinfo hints; // 填充getaddrinfo參數 struct addrinfo *result; // 存放getaddrinfo返回數據 memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = 0; hints.ai_protocol = 0; if(getaddrinfo(host, port, &hints, &result) != 0) { printf("getaddrinfo error"); exit(1); } return result; }
對了,getaddrinfo返回的result指向的內存是系統分配的,用完了要調用 freeaddrinfo去釋放內存的。其實getaddrinfo的內部實現挺復雜的,調用了一堆ga開頭 的函數,而且struct addrinfo其實也蠻復雜的,里面有好多信息,但用好它是寫出 同時支持ipv4,ipv6網絡程序的關鍵。
創建socket, 要熟悉下family,socktype,protocol等概念和取值,查man吧
int create_socket(const struct addrinfo * result) { int fd; if ((fd = socket(result->ai_family, result->ai_socktype, result->ai_protocol)) == -1) { printf("create socket error:%d\n", fd); exit(-1); } printf("cerate socket ok: %d\n", fd); return fd; }
連接目標主機, 這里其實就是要三步握手了,有幾個常見的錯誤,可以通過檢測errno來 讀取,如ETIMEDOUT表示建立連接超時,就是發出去sync沒人搭理,或ECONNREFUSED表示 對方端口沒開,發過去的sync直接被對方發了個rst回來,或EHOSTUNREACH表示對方機器 沒開或宕機了,因為ICMP包返回錯誤了。
int connect_host(int fd, const struct addrinfo* addr) { if (connect(fd , addr->ai_addr, addr->ai_addrlen) == -1) { printf("connect error.\n"); exit(-1); } printf("collect ok\n"); return 0; }
我們要做一個HTTP客戶端,類似curl,要拼一個HTTP請求發送給遠程主機,拼包用 snprintf雖然弱了一點,但也是最容易理解的,先用着。要留意格式化后的字符串大小 別超過緩沖區大小,當然了指定了長度不會溢出,但超過后會截斷,如果HTTP請求丟失 了最后的兩對\r\n,服務端就不知道客戶端發送完數據了, 所以這里邊界處理要十分 小心,可能我這里寫的還有BUG。
還有就是數據大的話send一次可能發送不完,這里先簡單粗暴處理了一下,真實程序的 話要把剩下的半拉重新拷貝個buf發出去的。
int get_send_data(char * buf, size_t buf_size, const char* host) { const char *send_tpl; // 數據模板,%s是host占位符 size_t to_send_size; // 要發送到數據大小 send_tpl = "GET / HTTP/1.1\r\n" "Host: %s\r\n" "Accept: */*\r\n" "\r\n\r\n"; // 格式化后的長度必須小於buf的大小,因為snprintf會在最后填個'\0' if (strlen(host) + strlen(send_tpl) - 2 >= buf_size) { // 2 = strlen("%s") printf("host too long.\n"); exit(-1); } to_send_size = snprintf(buf, buf_size, send_tpl, host); if (to_send_size < 0) { printf("snprintf error:%s.\n", to_send_size); exit(-2); } return to_send_size; } int send_data(int fd, const char *data, size_t size) { size_t sent_size; printf("will send:\n%s", data); sent_size = write(fd, data, size); if (sent_size < 0) { printf("send data error.\n"); exit(-1); }else if(sent_size != size){ printf("not all send.\n"); exit(-2); } printf("send data ok.\n"); return sent_size; }
完了收數據,我們只取HTTP應答第一行就好了,然后關閉連接。協議解析也簡單粗暴 找到\r\n就停止,真實程序可能要寫個狀態機來解析了。
int recv_data(int fd, char* buf, int size) { int i; int recv_size = read(fd, buf, size); if (recv_size < 0) { printf("recv data error:%d\n", (int)recv_size); exit(-1); } if (recv_size == 0) { printf("recv 0 size data.\n"); exit(-2); } // 只取HTTP first line for (i = 0; i < size - 1; i++) { if (buf[i] == '\r' && buf[i+1] == '\n') { buf[i] = '\0'; } } printf("recv data:%s\n", buf); } int close_socket(int fd) { if(close(fd) < 0){ printf("close socket errors\n"); exit(-1); } printf("close socket ok\n"); }
最后用main函數把他們串起來
int main(int argc, const char *argv[]) { const char* host = argv[1]; // 目標主機 char send_buff[SEND_BUF_SIZE]; // 發送緩沖區 char recv_buf[RECV_BUFF_SIZE]; // 接收緩沖區 size_t to_send_size = 0; // 要發送數據大小 int client_fd; // 客戶端socket struct addrinfo *addr; // 存放getaddrinfo返回數據 if (argc != 2) { printf("Usage:%s [host]\n", argv[0]); return 1; } addr = get_addr(host, "80"); client_fd = create_socket(addr); connect_host(client_fd, addr); freeaddrinfo(addr); to_send_size = get_send_data(send_buff, SEND_BUF_SIZE, host); send_data(client_fd, send_buff, to_send_size); recv_data(client_fd, recv_buf, RECV_BUFF_SIZE); close(client_fd); return 0; }
小結
多看,多寫,多練,肯定能熟悉C語言的,我現在看好多C的書都能看懂了。